commit 227f2047a64c377ce4baeb0541f923d8a372a206 Author: R. Eric Wheeler Date: Sat Jul 2 10:58:30 2016 -0700 Initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d4ff05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/phpunit.xml +/vendor +/build +/composer.lock + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..208a657 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,36 @@ +language: php + +sudo: false + +env: + global: + - SYMFONY_DEPRECATIONS_HELPER=weak + +cache: + directories: + - $HOME/.composer/cache + +before_install: + - if [[ $TRAVIS_PHP_VERSION != hhvm ]]; then phpenv config-rm xdebug.ini; fi + +before_script: + # symfony/* + - sh -c "if [ '$TWIG_VERSION' != '2.0' ]; then sed -i 's/~1.8|~2.0/~1.8/g' composer.json; composer update; fi" + - sh -c "if [ '$SYMFONY_DEPS_VERSION' = '3.0' ]; then sed -i 's/~2\.8|^3\.0/3.0.*@dev/g' composer.json; composer update; fi" + - sh -c "if [ '$SYMFONY_DEPS_VERSION' = '3.1' ]; then sed -i 's/~2\.8|^3\.0/3.1.*@dev/g' composer.json; composer update; fi" + - sh -c "if [ '$SYMFONY_DEPS_VERSION' = '' ]; then sed -i 's/~2\.8|^3\.0/2.8.*@dev/g' composer.json; composer update; fi" + - composer install + +script: phpunit + +matrix: + include: + - php: 5.5 + - php: 5.6 + env: TWIG_VERSION=2.0 + - php: 5.6 + env: SYMFONY_DEPS_VERSION=3.0 + - php: 5.6 + env: SYMFONY_DEPS_VERSION=3.1 + - php: 7.0 + - php: hhvm diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7c3778d --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010-2016 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..29c0999 --- /dev/null +++ b/README.rst @@ -0,0 +1,62 @@ +Silex, a simple Web Framework +============================= + +Silex is a PHP micro-framework to develop websites based on `Symfony +components`_:: + + get('/hello/{name}', function ($name) use ($app) { + return 'Hello '.$app->escape($name); + }); + + $app->run(); + +Silex works with PHP 5.5.9 or later. + +Installation +------------ + +The recommended way to install Silex is through `Composer`_: + +.. code-block:: bash + + composer require silex/silex "~2.0" + +Alternatively, you can download the `silex.zip`_ file and extract it. + +More Information +---------------- + +Read the `documentation`_ for more information and `changelog +`_ for upgrading information. + +Tests +----- + +To run the test suite, you need `Composer`_ and `PHPUnit`_: + +.. code-block:: bash + + $ composer install + $ phpunit + +Community +--------- + +Check out #silex-php on irc.freenode.net. + +License +------- + +Silex is licensed under the MIT license. + +.. _Symfony components: http://symfony.com +.. _Composer: http://getcomposer.org +.. _PHPUnit: https://phpunit.de +.. _silex.zip: http://silex.sensiolabs.org/download +.. _documentation: http://silex.sensiolabs.org/documentation diff --git a/bin/build b/bin/build new file mode 100755 index 0000000..e727003 --- /dev/null +++ b/bin/build @@ -0,0 +1,67 @@ +#!/bin/sh + +PHP=`which php` +GIT=`which git` +DIR=`$PHP -r "echo dirname(dirname(realpath('$0')));"` + +if [ ! -d "$DIR/build" ]; then + mkdir -p $DIR/build +fi + +cd $DIR/build + +if [ ! -f "composer.phar" ]; then + curl -s http://getcomposer.org/installer 2>/dev/null | $PHP >/dev/null 2>/dev/null +else + $PHP composer.phar self-update >/dev/null 2>/dev/null +fi + +for TYPE in slim fat +do + if [ -d "$DIR/build/skeleton" ]; then + rm -rf $DIR/build/skeleton + fi + mkdir -p $DIR/build/skeleton + + cd "$DIR/build/skeleton" + + mkdir -p web/ + COMPOSER=$TYPE"_composer.json" + cp $DIR/bin/skeleton/$COMPOSER composer.json + cp $DIR/bin/skeleton/index.php web/index.php + + $PHP ../composer.phar install -q + + if [ -d "$DIR/build/tmp/silex" ]; then + rm -rf $DIR/build/tmp/silex + fi + mkdir -p $DIR/build/tmp/silex + + cd "$DIR/build/tmp/silex" + cp -r ../../skeleton/* . + + find . -name .DS_Store | xargs rm -rf - + find . -name .git | xargs rm -rf - + find . -name phpunit.xml.* | xargs rm -rf - + find . -type d -name Tests | xargs rm -rf - + find . -type d -name test* | xargs rm -rf - + find . -type d -name doc | xargs rm -rf - + find . -type d -name ext | xargs rm -rf - + + export COPY_EXTENDED_ATTRIBUTES_DISABLE=true + export COPYFILE_DISABLE=true + + cd "$DIR/build/tmp" + + if [ "slim" = "$TYPE" ]; then + NAME="silex" + else + NAME="silex_fat" + fi + + rm -f "$DIR/build/$NAME.*" + tar zcpf "$DIR/build/$NAME.tgz" silex + zip -rq "$DIR/build/$NAME.zip" silex + rm -rf "$DIR/build/tmp" + rm -rf "$DIR/build/skeleton" +done diff --git a/bin/skeleton/fat_composer.json b/bin/skeleton/fat_composer.json new file mode 100644 index 0000000..4495d4f --- /dev/null +++ b/bin/skeleton/fat_composer.json @@ -0,0 +1,23 @@ +{ + "require": { + "silex/silex": "~1.1", + "symfony/browser-kit": "~2.3", + "symfony/console": "~2.3", + "symfony/config": "~2.3", + "symfony/css-selector": "~2.3", + "symfony/dom-crawler": "~2.3", + "symfony/filesystem": "~2.3", + "symfony/finder": "~2.3", + "symfony/form": "~2.3", + "symfony/locale": "~2.3", + "symfony/process": "~2.3", + "symfony/security": "~2.3", + "symfony/serializer": "~2.3", + "symfony/translation": "~2.3", + "symfony/validator": "~2.3", + "symfony/monolog-bridge": "~2.3", + "symfony/twig-bridge": "~2.3", + "doctrine/dbal": ">=2.2.0,<2.4.0-dev", + "swiftmailer/swiftmailer": "5.*" + } +} diff --git a/bin/skeleton/index.php b/bin/skeleton/index.php new file mode 100644 index 0000000..683c610 --- /dev/null +++ b/bin/skeleton/index.php @@ -0,0 +1,11 @@ +get('/hello', function () { + return 'Hello!'; +}); + +$app->run(); diff --git a/bin/skeleton/slim_composer.json b/bin/skeleton/slim_composer.json new file mode 100644 index 0000000..df5ed00 --- /dev/null +++ b/bin/skeleton/slim_composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "silex/silex": "~1.1" + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a4cd228 --- /dev/null +++ b/composer.json @@ -0,0 +1,76 @@ +{ + "name": "silex/silex", + "description": "The PHP micro-framework based on the Symfony Components", + "keywords": ["microframework"], + "homepage": "http://silex.sensiolabs.org", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "require": { + "php": ">=5.5.9", + "pimple/pimple": "~3.0", + "symfony/event-dispatcher": "~2.8|^3.0", + "symfony/http-foundation": "~2.8|^3.0", + "symfony/http-kernel": "~2.8|^3.0", + "symfony/routing": "~2.8|^3.0", + "knplabs/knp-snappy": "^0.4.3", + "h4cc/wkhtmltopdf-amd64": "0.12.x", + "h4cc/wkhtmltoimage-amd64": "0.12.x", + "webmozart/json": "^1.2" + }, + "require-dev": { + "symfony/asset": "~2.8|^3.0", + "symfony/expression-language": "~2.8|^3.0", + "symfony/security": "~2.8|^3.0", + "symfony/config": "~2.8|^3.0", + "symfony/form": "~2.8|^3.0", + "symfony/browser-kit": "~2.8|^3.0", + "symfony/css-selector": "~2.8|^3.0", + "symfony/debug": "~2.8|^3.0", + "symfony/dom-crawler": "~2.8|^3.0", + "symfony/finder": "~2.8|^3.0", + "symfony/intl": "~2.8|^3.0", + "symfony/monolog-bridge": "~2.8|^3.0", + "symfony/doctrine-bridge": "~2.8|^3.0", + "symfony/options-resolver": "~2.8|^3.0", + "symfony/phpunit-bridge": "~2.8|^3.0", + "symfony/process": "~2.8|^3.0", + "symfony/serializer": "~2.8|^3.0", + "symfony/translation": "~2.8|^3.0", + "symfony/twig-bridge": "~2.8|^3.0", + "symfony/validator": "~2.8|^3.0", + "symfony/var-dumper": "~2.8|^3.0", + "twig/twig": "~1.8|~2.0", + "doctrine/dbal": "~2.2", + "swiftmailer/swiftmailer": "~5", + "monolog/monolog": "^1.4.1", + "symfony/console": "^3.1" + }, + "replace": { + "silex/api": "v2.0.2", + "silex/providers": "v2.0.2" + }, + "autoload": { + "psr-4": { + "Silex\\": "src/Silex", + "Sikofitt\\": "src/Sikofitt" + } + }, + "autoload-dev" : { + "psr-4": { "Silex\\Tests\\" : "tests/Silex/Tests" } + }, + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "minimum-stability": "dev" +} diff --git a/doc/changelog.rst b/doc/changelog.rst new file mode 100644 index 0000000..0c9cdd4 --- /dev/null +++ b/doc/changelog.rst @@ -0,0 +1,353 @@ +Changelog +========= + +2.0.2 (2016-06-14) +------------------ + +* fixed Symfony 3.1 deprecations + +2.0.1 (2016-05-27) +------------------ + +* fixed the silex form extension registration to allow overriding default ones +* removed support for the obsolete Locale Symfony component (uses the Intl one now) +* added support for Symfony 3.1 + +2.0.0 (2016-05-18) +------------------ + +* decoupled the exception handler from HttpKernelServiceProvider +* Switched to BCrypt as the default encoder in the security provider +* added full support for RequestMatcher +* added support for Symfony Guard +* added support for callables in CallbackResolver +* added FormTrait::namedForm() +* added support for delivery_addresses, delivery_whitelist, and sender_address +* added support to register form types / form types extensions / form types guessers as services +* added support for callable in mounts (allow nested route collection to be built easily) +* added support for conditions on routes +* added support for the Symfony VarDumper Component +* added a global Twig variable (an AppVariable instance) +* [BC BREAK] CSRF has been moved to a standalone provider (``form.secret`` is not available anymore) +* added support for the Symfony HttpFoundation Twig bridge extension +* added support for the Symfony Asset Component +* bumped minimum version of Symfony to 2.8 +* bumped minimum version of PHP to 5.5.0 +* Updated Pimple to 3.0 +* Updated session listeners to extends HttpKernel ones +* [BC BREAK] Locale management has been moved to LocaleServiceProvider which must be registered + if you want Silex to manage your locale (must also be registered for the translation service provider) +* [BC BREAK] Provider interfaces moved to Silex\Api namespace, published as + separate package via subtree split +* [BC BREAK] ServiceProviderInterface split in to EventListenerProviderInterface + and BootableProviderInterface +* [BC BREAK] Service Provider support files moved under Silex\Provider + namespace, allowing publishing as separate package via sub-tree split +* ``monolog.exception.logger_filter`` option added to Monolog service provider +* [BC BREAK] ``$app['request']`` service removed, use ``$app['request_stack']`` instead + +1.3.6 (2016-XX-XX) +------------------ + +* n/a + +1.3.5 (2016-01-06) +------------------ + +* fixed typo in SecurityServiceProvider + +1.3.4 (2015-09-15) +------------------ + +* fixed some new deprecations +* fixed translation registration for the validators + +1.3.3 (2015-09-08) +------------------ + +* added support for Symfony 3.0 and Twig 2.0 +* fixed some Form deprecations +* removed deprecated method call in the exception handler +* fixed Swiftmailer spool flushing when spool is not enabled + +1.3.2 (2015-08-24) +------------------ + +* no changes + +1.3.1 (2015-08-04) +------------------ + +* added missing support for the Expression constraint +* fixed the possibility to override translations for validator error messages +* fixed sub-mounts with same name clash +* fixed session logout handler when a firewall is stateless + +1.3.0 (2015-06-05) +------------------ + +* added a `$app['user']` to get the current user (security provider) +* added view handlers +* added support for the OPTIONS HTTP method +* added caching for the Translator provider +* deprecated `$app['exception_handler']->disable()` in favor of `unset($app['exception_handler'])` +* made Silex compatible with Symfony 2.7 an 2.8 (and keep compatibility with Symfony 2.3, 2.5, and 2.6) +* removed deprecated TwigCoreExtension class (register the new HttpFragmentServiceProvider instead) +* bumped minimum version of PHP to 5.3.9 + +1.2.5 (2015-06-04) +------------------ + +* no code changes (last version of the 1.2 branch) + +1.2.4 (2015-04-11) +------------------ + +* fixed the exception message when mounting a collection that doesn't return a ControllerCollection +* fixed Symfony dependencies (Silex 1.2 is not compatible with Symfony 2.7) + +1.2.3 (2015-01-20) +------------------ + +* fixed remember me listener +* fixed translation files loading when they do not exist +* allowed global after middlewares to return responses like route specific ones + +1.2.2 (2014-09-26) +------------------ + +* fixed Translator locale management +* added support for the $app argument in application middlewares (to make it consistent with route middlewares) +* added form.types to the Form provider + +1.2.1 (2014-07-01) +------------------ + +* added support permissions in the Monolog provider +* fixed Switfmailer spool where the event dispatcher is different from the other ones +* fixed locale when changing it on the translator itself + +1.2.0 (2014-03-29) +------------------ + +* Allowed disabling the boot logic of MonologServiceProvider +* Reverted "convert attributes on the request that actually exist" +* [BC BREAK] Routes are now always added in the order of their registration (even for mounted routes) +* Added run() on Route to be able to define the controller code +* Deprecated TwigCoreExtension (register the new HttpFragmentServiceProvider instead) +* Added HttpFragmentServiceProvider +* Allowed a callback to be a method call on a service (before, after, finish, error, on Application; convert, before, after on Controller) + +1.1.3 (2013-XX-XX) +------------------ + +* Fixed translator locale management + +1.1.2 (2013-10-30) +------------------ + +* Added missing "security.hide_user_not_found" support in SecurityServiceProvider +* Fixed event listeners that are registered after the boot via the on() method + +1.0.2 (2013-10-30) +------------------ + +* Fixed SecurityServiceProvider to use null as a fake controller so that routes can be dumped + +1.1.1 (2013-10-11) +------------------ + +* Removed or replaced deprecated Symfony code +* Updated code to take advantages of 2.3 new features +* Only convert attributes on the request that actually exist. + +1.1.0 (2013-07-04) +------------------ + +* Support for any ``Psr\Log\LoggerInterface`` as opposed to the monolog-bridge + one. +* Made dispatcher proxy methods ``on``, ``before``, ``after`` and ``error`` + lazy, so that they will not instantiate the dispatcher early. +* Dropped support for 2.1 and 2.2 versions of Symfony. + +1.0.1 (2013-07-04) +------------------ + +* Fixed RedirectableUrlMatcher::redirect() when Silex is configured to use a logger +* Make ``DoctrineServiceProvider`` multi-db support lazy. + +1.0.0 (2013-05-03) +------------------ + +* **2013-04-12**: Added support for validators as services. + +* **2013-04-01**: Added support for host matching with symfony 2.2:: + + $app->match('/', function() { + // app-specific action + })->host('example.com'); + + $app->match('/', function ($user) { + // user-specific action + })->host('{user}.example.com'); + +* **2013-03-08**: Added support for form type extensions and guessers as + services. + +* **2013-03-08**: Added support for remember-me via the + ``RememberMeServiceProvider``. + +* **2013-02-07**: Added ``Application::sendFile()`` to ease sending + ``BinaryFileResponse``. + +* **2012-11-05**: Filters have been renamed to application middlewares in the + documentation. + +* **2012-11-05**: The ``before()``, ``after()``, ``error()``, and ``finish()`` + listener priorities now set the priority of the underlying Symfony event + instead of a custom one before. + +* **2012-11-05**: Removing the default exception handler should now be done + via its ``disable()`` method: + + Before: + + unset($app['exception_handler']); + + After: + + $app['exception_handler']->disable(); + +* **2012-07-15**: removed the ``monolog.configure`` service. Use the + ``extend`` method instead: + + Before:: + + $app['monolog.configure'] = $app->protect(function ($monolog) use ($app) { + // do something + }); + + After:: + + $app['monolog'] = $app->share($app->extend('monolog', function($monolog, $app) { + // do something + + return $monolog; + })); + + +* **2012-06-17**: ``ControllerCollection`` now takes a required route instance + as a constructor argument. + + Before:: + + $controllers = new ControllerCollection(); + + After:: + + $controllers = new ControllerCollection(new Route()); + + // or even better + $controllers = $app['controllers_factory']; + +* **2012-06-17**: added application traits for PHP 5.4 + +* **2012-06-16**: renamed ``request.default_locale`` to ``locale`` + +* **2012-06-16**: Removed the ``translator.loader`` service. See documentation + for how to use XLIFF or YAML-based translation files. + +* **2012-06-15**: removed the ``twig.configure`` service. Use the ``extend`` + method instead: + + Before:: + + $app['twig.configure'] = $app->protect(function ($twig) use ($app) { + // do something + }); + + After:: + + $app['twig'] = $app->share($app->extend('twig', function($twig, $app) { + // do something + + return $twig; + })); + +* **2012-06-13**: Added a route ``before`` middleware + +* **2012-06-13**: Renamed the route ``middleware`` to ``before`` + +* **2012-06-13**: Added an extension for the Symfony Security component + +* **2012-05-31**: Made the ``BrowserKit``, ``CssSelector``, ``DomCrawler``, + ``Finder`` and ``Process`` components optional dependencies. Projects that + depend on them (e.g. through functional tests) should add those dependencies + to their ``composer.json``. + +* **2012-05-26**: added ``boot()`` to ``ServiceProviderInterface``. + +* **2012-05-26**: Removed ``SymfonyBridgesServiceProvider``. It is now implicit + by checking the existence of the bridge. + +* **2012-05-26**: Removed the ``translator.messages`` parameter (use + ``translator.domains`` instead). + +* **2012-05-24**: Removed the ``autoloader`` service (use composer instead). + The ``*.class_path`` settings on all the built-in providers have also been + removed in favor of Composer. + +* **2012-05-21**: Changed error() to allow handling specific exceptions. + +* **2012-05-20**: Added a way to define settings on a controller collection. + +* **2012-05-20**: The Request instance is not available anymore from the + Application after it has been handled. + +* **2012-04-01**: Added ``finish`` filters. + +* **2012-03-20**: Added ``json`` helper:: + + $data = array('some' => 'data'); + $response = $app->json($data); + +* **2012-03-11**: Added route middlewares. + +* **2012-03-02**: Switched to use Composer for dependency management. + +* **2012-02-27**: Updated to Symfony 2.1 session handling. + +* **2012-01-02**: Introduced support for streaming responses. + +* **2011-09-22**: ``ExtensionInterface`` has been renamed to + ``ServiceProviderInterface``. All built-in extensions have been renamed + accordingly (for instance, ``Silex\Extension\TwigExtension`` has been + renamed to ``Silex\Provider\TwigServiceProvider``). + +* **2011-09-22**: The way reusable applications work has changed. The + ``mount()`` method now takes an instance of ``ControllerCollection`` instead + of an ``Application`` one. + + Before:: + + $app = new Application(); + $app->get('/bar', function() { return 'foo'; }); + + return $app; + + After:: + + $app = new ControllerCollection(); + $app->get('/bar', function() { return 'foo'; }); + + return $app; + +* **2011-08-08**: The controller method configuration is now done on the Controller itself + + Before:: + + $app->match('/', function () { echo 'foo'; }, 'GET|POST'); + + After:: + + $app->match('/', function () { echo 'foo'; })->method('GET|POST'); diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 0000000..dfe355c --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,17 @@ +import sys, os +from sphinx.highlighting import lexers +from pygments.lexers.web import PhpLexer + +sys.path.append(os.path.abspath('_exts')) + +extensions = [] +master_doc = 'index' +highlight_language = 'php' + +project = u'Silex' +copyright = u'2010 Fabien Potencier' + +version = '0' +release = '0.0.0' + +lexers['php'] = PhpLexer(startinline=True) diff --git a/doc/contributing.rst b/doc/contributing.rst new file mode 100644 index 0000000..34a339d --- /dev/null +++ b/doc/contributing.rst @@ -0,0 +1,34 @@ +Contributing +============ + +We are open to contributions to the Silex code. If you find a bug or want to +contribute a provider, just follow these steps: + +* Fork `the Silex repository `_; + +* Make your feature addition or bug fix; + +* Add tests for it; + +* Optionally, add some documentation; + +* `Send a pull request + `_, to the correct + target branch (1.3 for bug fixes, master for new features). + +.. note:: + + Any code you contribute must be licensed under the MIT + License. + +Writing Documentation +===================== + +The documentation is written in `reStructuredText +`_ and can be generated using `sphinx +`_. + +.. code-block:: bash + + $ cd doc + $ sphinx-build -b html . build diff --git a/doc/cookbook/error_handler.rst b/doc/cookbook/error_handler.rst new file mode 100644 index 0000000..235c263 --- /dev/null +++ b/doc/cookbook/error_handler.rst @@ -0,0 +1,38 @@ +Converting Errors to Exceptions +=============================== + +Silex catches exceptions that are thrown from within a request/response cycle. +However, it does *not* catch PHP errors and notices. This recipe tells you how +to catch them by converting them to exceptions. + +Registering the ErrorHandler +---------------------------- + +The ``Symfony/Debug`` package has an ``ErrorHandler`` class that solves this +problem. It converts all errors to exceptions, and exceptions are then caught +by Silex. + +Register it by calling the static ``register`` method:: + + use Symfony\Component\Debug\ErrorHandler; + + ErrorHandler::register(); + +It is recommended that you do this as early as possible. + +Handling fatal errors +--------------------- + +To handle fatal errors, you can additionally register a global +``ExceptionHandler``:: + + use Symfony\Component\Debug\ExceptionHandler; + + ExceptionHandler::register(); + +In production you may want to disable the debug output by passing ``false`` as +the ``$debug`` argument:: + + use Symfony\Component\Debug\ExceptionHandler; + + ExceptionHandler::register(false); diff --git a/doc/cookbook/form_no_csrf.rst b/doc/cookbook/form_no_csrf.rst new file mode 100644 index 0000000..e9bf595 --- /dev/null +++ b/doc/cookbook/form_no_csrf.rst @@ -0,0 +1,36 @@ +Disabling CSRF Protection on a Form using the FormExtension +=========================================================== + +The *FormExtension* provides a service for building form in your application +with the Symfony Form component. When the :doc:`CSRF Service Provider +` is registered, the *FormExtension* uses the CSRF Protection +avoiding Cross-site request forgery, a method by which a malicious user +attempts to make your legitimate users unknowingly submit data that they don't +intend to submit. + +You can find more details about CSRF Protection and CSRF token in the +`Symfony Book +`_. + +In some cases (for example, when embedding a form in an html email) you might +want not to use this protection. The easiest way to avoid this is to +understand that it is possible to give specific options to your form builder +through the ``createBuilder()`` function. + +Example +------- + +.. code-block:: php + + $form = $app['form.factory']->createBuilder('form', null, array('csrf_protection' => false)); + +That's it, your form could be submitted from everywhere without CSRF Protection. + +Going further +------------- + +This specific example showed how to change the ``csrf_protection`` in the +``$options`` parameter of the ``createBuilder()`` function. More of them could +be passed through this parameter, it is as simple as using the Symfony +``getDefaultOptions()`` method in your form classes. `See more here +`_. diff --git a/doc/cookbook/guard_authentication.rst b/doc/cookbook/guard_authentication.rst new file mode 100644 index 0000000..f7f736c --- /dev/null +++ b/doc/cookbook/guard_authentication.rst @@ -0,0 +1,182 @@ +How to Create a Custom Authentication System with Guard +======================================================= + +Whether you need to build a traditional login form, an API token +authentication system or you need to integrate with some proprietary +single-sign-on system, the Guard component can make it easy... and fun! + +In this example, you'll build an API token authentication system and +learn how to work with Guard. + +Step 1) Create the Authenticator Class +-------------------------------------- + +Suppose you have an API where your clients will send an X-AUTH-TOKEN +header on each request. This token is composed of the username followed +by a password, separated by a colon (e.g. ``X-AUTH-TOKEN: coolguy:awesomepassword``). +Your job is to read this, find theassociated user (if any) and check +the password. + +To create a custom authentication system, just create a class and make +it implement GuardAuthenticatorInterface. Or, extend the simpler +AbstractGuardAuthenticator. This requires you to implement six methods: + +.. code-block:: php + + encoderFactory = $encoderFactory; + } + + public function getCredentials(Request $request) + { + // Checks if the credential header is provided + if (!$token = $request->headers->get('X-AUTH-TOKEN')) { + return; + } + + // Parse the header or ignore it if the format is incorrect. + if (false === strpos(':', $token)) { + return; + } + list($username, $secret) = explode(':', $token, 2); + + return array( + 'username' => $username, + 'secret' => $secret, + ); + } + + public function getUser($credentials, UserProviderInterface $userProvider) + { + return $userProvider->loadUserByUsername($credentials['username']); + } + + public function checkCredentials($credentials, UserInterface $user) + { + // check credentials - e.g. make sure the password is valid + // return true to cause authentication success + + $encoder = $this->encoderFactory->getEncoder($user); + + return $encoder->isPasswordValid( + $user->getPassword(), + $credentials['secret'], + $user->getSalt() + ); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + { + // on success, let the request continue + return; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + { + $data = array( + 'message' => strtr($exception->getMessageKey(), $exception->getMessageData()), + + // or to translate this message + // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData()) + ); + + return new JsonResponse($data, 403); + } + + /** + * Called when authentication is needed, but it's not sent + */ + public function start(Request $request, AuthenticationException $authException = null) + { + $data = array( + // you might translate this message + 'message' => 'Authentication Required', + ); + + return new JsonResponse($data, 401); + } + + public function supportsRememberMe() + { + return false; + } + } + + +Step 2) Configure the Authenticator +----------------------------------- + +To finish this, register the class as a service: + +.. code-block:: php + + $app['app.token_authenticator'] = function ($app) { + return new App\Security\TokenAuthenticator($app['security.encoder_factory']); + }; + + +Finally, configure your `security.firewalls` key to use this authenticator: + +.. code-block:: php + + $app['security.firewalls'] => array( + 'main' => array( + 'guard' => array( + 'authenticators' => array( + 'app.token_authenticator' + ), + + // Using more than 1 authenticator, you must specify + // which one is used as entry point. + // 'entry_point' => 'app.token_authenticator', + ), + // configure where your users come from. Hardcode them, or load them from somewhere + // http://silex.sensiolabs.org/doc/providers/security.html#defining-a-custom-user-provider + 'users' => array( + 'victoria' => array('ROLE_USER', 'randomsecret'), + ), + // 'anonymous' => true + ), + ); + +.. note:: + You can use many authenticators, they are executed by the order + they are configured. + +You did it! You now have a fully-working API token authentication +system. If your homepage required ROLE_USER, then you could test it +under different conditions: + +.. code-block:: bash + + # test with no token + curl http://localhost:8000/ + # {"message":"Authentication Required"} + + # test with a bad token + curl -H "X-AUTH-TOKEN: alan" http://localhost:8000/ + # {"message":"Username could not be found."} + + # test with a working token + curl -H "X-AUTH-TOKEN: victoria:randomsecret" http://localhost:8000/ + # the homepage controller is executed: the page loads normally + +For more details read the Symfony cookbook entry on +`How to Create a Custom Authentication System with Guard `_. diff --git a/doc/cookbook/index.rst b/doc/cookbook/index.rst new file mode 100644 index 0000000..53b10fe --- /dev/null +++ b/doc/cookbook/index.rst @@ -0,0 +1,40 @@ +Cookbook +======== + +The cookbook section contains recipes for solving specific problems. + +.. toctree:: + :maxdepth: 1 + :hidden: + + json_request_body + session_storage + form_no_csrf + validator_yaml + sub_requests + error_handler + multiple_loggers + guard_authentication + +Recipes +------- + +* :doc:`Accepting a JSON Request Body ` A common need when + building a restful API is the ability to accept a JSON encoded entity from + the request body. + +* :doc:`Using PdoSessionStorage to store Sessions in the Database + `. + +* :doc:`Disabling the CSRF Protection on a Form using the FormExtension + `. + +* :doc:`Using YAML to configure Validation `. + +* :doc:`Making sub-Requests `. + +* :doc:`Converting Errors to Exceptions `. + +* :doc:`Using multiple Monolog Loggers `. + +* :doc:`How to Create a Custom Authentication System with Guard `. diff --git a/doc/cookbook/json_request_body.rst b/doc/cookbook/json_request_body.rst new file mode 100644 index 0000000..4715900 --- /dev/null +++ b/doc/cookbook/json_request_body.rst @@ -0,0 +1,95 @@ +Accepting a JSON Request Body +============================= + +A common need when building a restful API is the ability to accept a JSON +encoded entity from the request body. + +An example for such an API could be a blog post creation. + +Example API +----------- + +In this example we will create an API for creating a blog post. The following +is a spec of how we want it to work. + +Request +~~~~~~~ + +In the request we send the data for the blog post as a JSON object. We also +indicate that using the ``Content-Type`` header: + +.. code-block:: text + + POST /blog/posts + Accept: application/json + Content-Type: application/json + Content-Length: 57 + + {"title":"Hello World!","body":"This is my first post!"} + +Response +~~~~~~~~ + +The server responds with a 201 status code, telling us that the post was +created. It tells us the ``Content-Type`` of the response, which is also +JSON: + +.. code-block:: text + + HTTP/1.1 201 Created + Content-Type: application/json + Content-Length: 65 + Connection: close + + {"id":"1","title":"Hello World!","body":"This is my first post!"} + +Parsing the request body +------------------------ + +The request body should only be parsed as JSON if the ``Content-Type`` header +begins with ``application/json``. Since we want to do this for every request, +the easiest solution is to use an application before middleware. + +We simply use ``json_decode`` to parse the content of the request and then +replace the request data on the ``$request`` object:: + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\ParameterBag; + + $app->before(function (Request $request) { + if (0 === strpos($request->headers->get('Content-Type'), 'application/json')) { + $data = json_decode($request->getContent(), true); + $request->request->replace(is_array($data) ? $data : array()); + } + }); + +Controller implementation +------------------------- + +Our controller will create a new blog post from the data provided and will +return the post object, including its ``id``, as JSON:: + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + $app->post('/blog/posts', function (Request $request) use ($app) { + $post = array( + 'title' => $request->request->get('title'), + 'body' => $request->request->get('body'), + ); + + $post['id'] = createPost($post); + + return $app->json($post, 201); + }); + +Manual testing +-------------- + +In order to manually test our API, we can use the ``curl`` command line +utility, which allows sending HTTP requests: + +.. code-block:: bash + + $ curl http://blog.lo/blog/posts -d '{"title":"Hello World!","body":"This is my first post!"}' -H 'Content-Type: application/json' + {"id":"1","title":"Hello World!","body":"This is my first post!"} diff --git a/doc/cookbook/multiple_loggers.rst b/doc/cookbook/multiple_loggers.rst new file mode 100644 index 0000000..9bc33e7 --- /dev/null +++ b/doc/cookbook/multiple_loggers.rst @@ -0,0 +1,69 @@ +Using multiple Monolog Loggers +============================== + +Having separate instances of Monolog for different parts of your system is +often desirable and allows you to configure them independently, allowing for fine +grained control of where your logging goes and in what detail. + +This simple example allows you to quickly configure several monolog instances, +using the bundled handler, but each with a different channel. + +.. code-block:: php + + $app['monolog.factory'] = $app->protect(function ($name) use ($app) { + $log = new $app['monolog.logger.class']($name); + $log->pushHandler($app['monolog.handler']); + + return $log; + }); + + foreach (array('auth', 'payments', 'stats') as $channel) { + $app['monolog.'.$channel] = function ($app) use ($channel) { + return $app['monolog.factory']($channel); + }; + } + +As your application grows, or your logging needs for certain areas of the +system become apparent, it should be straightforward to then configure that +particular service separately, including your customizations. + +.. code-block:: php + + use Monolog\Handler\StreamHandler; + + $app['monolog.payments'] = function ($app) { + $log = new $app['monolog.logger.class']('payments'); + $handler = new StreamHandler($app['monolog.payments.logfile'], $app['monolog.payment.level']); + $log->pushHandler($handler); + + return $log; + }; + +Alternatively, you could attempt to make the factory more complicated, and rely +on some conventions, such as checking for an array of handlers registered with +the container with the channel name, defaulting to the bundled handler. + +.. code-block:: php + + use Monolog\Handler\StreamHandler; + use Monolog\Logger; + + $app['monolog.factory'] = $app->protect(function ($name) use ($app) { + $log = new $app['monolog.logger.class']($name); + + $handlers = isset($app['monolog.'.$name.'.handlers']) + ? $app['monolog.'.$name.'.handlers'] + : array($app['monolog.handler']); + + foreach ($handlers as $handler) { + $log->pushHandler($handler); + } + + return $log; + }); + + $app['monolog.payments.handlers'] = function ($app) { + return array( + new StreamHandler(__DIR__.'/../payments.log', Logger::DEBUG), + ); + }; diff --git a/doc/cookbook/session_storage.rst b/doc/cookbook/session_storage.rst new file mode 100644 index 0000000..8741ad5 --- /dev/null +++ b/doc/cookbook/session_storage.rst @@ -0,0 +1,93 @@ +Using PdoSessionStorage to store Sessions in the Database +========================================================= + +By default, the :doc:`SessionServiceProvider ` writes +session information in files using Symfony NativeFileSessionStorage. Most +medium to large websites use a database to store sessions instead of files, +because databases are easier to use and scale in a multi-webserver environment. + +Symfony's `NativeSessionStorage +`_ +has multiple storage handlers and one of them uses PDO to store sessions, +`PdoSessionHandler +`_. +To use it, replace the ``session.storage.handler`` service in your application +like explained below. + +With a dedicated PDO service +---------------------------- + +.. code-block:: php + + use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; + + $app->register(new Silex\Provider\SessionServiceProvider()); + + $app['pdo.dsn'] = 'mysql:dbname=mydatabase'; + $app['pdo.user'] = 'myuser'; + $app['pdo.password'] = 'mypassword'; + + $app['session.db_options'] = array( + 'db_table' => 'session', + 'db_id_col' => 'session_id', + 'db_data_col' => 'session_value', + 'db_time_col' => 'session_time', + ); + + $app['pdo'] = function () use ($app) { + return new PDO( + $app['pdo.dsn'], + $app['pdo.user'], + $app['pdo.password'] + ); + }; + + $app['session.storage.handler'] = function () use ($app) { + return new PdoSessionHandler( + $app['pdo'], + $app['session.db_options'], + $app['session.storage.options'] + ); + }; + +Using the DoctrineServiceProvider +--------------------------------- + +When using the :doc:`DoctrineServiceProvider ` You don't +have to make another database connection, simply pass the getWrappedConnection method. + +.. code-block:: php + + use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; + + $app->register(new Silex\Provider\SessionServiceProvider()); + + $app['session.db_options'] = array( + 'db_table' => 'session', + 'db_id_col' => 'session_id', + 'db_data_col' => 'session_value', + 'db_lifetime_col' => 'session_lifetime', + 'db_time_col' => 'session_time', + ); + + $app['session.storage.handler'] = function () use ($app) { + return new PdoSessionHandler( + $app['db']->getWrappedConnection(), + $app['session.db_options'], + $app['session.storage.options'] + ); + }; + +Database structure +------------------ + +PdoSessionStorage needs a database table with 3 columns: + +* ``session_id``: ID column (VARCHAR(255) or larger) +* ``session_value``: Value column (TEXT or CLOB) +* ``session_lifetime``: Lifetime column (INTEGER) +* ``session_time``: Time column (INTEGER) + +You can find examples of SQL statements to create the session table in the +`Symfony cookbook +`_ diff --git a/doc/cookbook/sub_requests.rst b/doc/cookbook/sub_requests.rst new file mode 100644 index 0000000..95d3913 --- /dev/null +++ b/doc/cookbook/sub_requests.rst @@ -0,0 +1,137 @@ +Making sub-Requests +=================== + +Since Silex is based on the ``HttpKernelInterface``, it allows you to simulate +requests against your application. This means that you can embed a page within +another, it also allows you to forward a request which is essentially an +internal redirect that does not change the URL. + +Basics +------ + +You can make a sub-request by calling the ``handle`` method on the +``Application``. This method takes three arguments: + +* ``$request``: An instance of the ``Request`` class which represents the + HTTP request. + +* ``$type``: Must be either ``HttpKernelInterface::MASTER_REQUEST`` or + ``HttpKernelInterface::SUB_REQUEST``. Certain listeners are only executed for + the master request, so it's important that this is set to ``SUB_REQUEST``. + +* ``$catch``: Catches exceptions and turns them into a response with status code + ``500``. This argument defaults to ``true``. For sub-requests you will most + likely want to set it to ``false``. + +By calling ``handle``, you can make a sub-request manually. Here's an example:: + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpKernel\HttpKernelInterface; + + $subRequest = Request::create('/'); + $response = $app->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false); + +There's some more things that you need to keep in mind though. In most cases +you will want to forward some parts of the current master request to the +sub-request like cookies, server information, or the session. + +Here is a more advanced example that forwards said information (``$request`` +holds the master request):: + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpKernel\HttpKernelInterface; + + $subRequest = Request::create('/', 'GET', array(), $request->cookies->all(), array(), $request->server->all()); + if ($request->getSession()) { + $subRequest->setSession($request->getSession()); + } + + $response = $app->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false); + +To forward this response to the client, you can simply return it from a +controller:: + + use Silex\Application; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpKernel\HttpKernelInterface; + + $app->get('/foo', function (Application $app, Request $request) { + $subRequest = Request::create('/', ...); + $response = $app->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false); + + return $response; + }); + +If you want to embed the response as part of a larger page you can call +``Response::getContent``:: + + $header = ...; + $footer = ...; + $body = $response->getContent(); + + return $header.$body.$footer; + +Rendering pages in Twig templates +--------------------------------- + +The :doc:`TwigServiceProvider ` provides a ``render`` +function that you can use in Twig templates. It gives you a convenient way to +embed pages. + +.. code-block:: jinja + + {{ render('/sidebar') }} + +For details, refer to the :doc:`TwigServiceProvider ` docs. + +Edge Side Includes +------------------ + +You can use ESI either through the :doc:`HttpCacheServiceProvider +` or a reverse proxy cache such as Varnish. This also +allows you to embed pages, however it also gives you the benefit of caching +parts of the page. + +Here is an example of how you would embed a page via ESI: + +.. code-block:: jinja + + + +For details, refer to the :doc:`HttpCacheServiceProvider +` docs. + +Dealing with the request base URL +--------------------------------- + +One thing to watch out for is the base URL. If your application is not +hosted at the webroot of your web server, then you may have an URL like +``http://example.org/foo/index.php/articles/42``. + +In this case, ``/foo/index.php`` is your request base path. Silex accounts for +this path prefix in the routing process, it reads it from +``$request->server``. In the context of sub-requests this can lead to issues, +because if you do not prepend the base path the request could mistake a part +of the path you want to match as the base path and cut it off. + +You can prevent that from happening by always prepending the base path when +constructing a request:: + + $url = $request->getUriForPath('/'); + $subRequest = Request::create($url, 'GET', array(), $request->cookies->all(), array(), $request->server->all()); + +This is something to be aware of when making sub-requests by hand. + +Services depending on the Request +--------------------------------- + +The container is a concept that is global to a Silex application, since the +application object **is** the container. Any request that is run against an +application will re-use the same set of services. Since these services are +mutable, code in a master request can affect the sub-requests and vice versa. +Any services depending on the ``request`` service will store the first request +that they get (could be master or sub-request), and keep using it, even if +that request is already over. + +Instead of injecting the ``request`` service, you should always inject the +``request_stack`` one instead. diff --git a/doc/cookbook/validator_yaml.rst b/doc/cookbook/validator_yaml.rst new file mode 100644 index 0000000..2d478ff --- /dev/null +++ b/doc/cookbook/validator_yaml.rst @@ -0,0 +1,35 @@ +Using YAML to configure Validation +================================== + +Simplicity is at the heart of Silex so there is no out of the box solution to +use YAML files for validation. But this doesn't mean that this is not +possible. Let's see how to do it. + +First, you need to install the YAML Component: + +.. code-block:: bash + + composer require symfony/yaml + +Next, you need to tell the Validation Service that you are not using +``StaticMethodLoader`` to load your class metadata but a YAML file:: + + $app->register(new ValidatorServiceProvider()); + + $app['validator.mapping.class_metadata_factory'] = new Symfony\Component\Validator\Mapping\ClassMetadataFactory( + new Symfony\Component\Validator\Mapping\Loader\YamlFileLoader(__DIR__.'/validation.yml') + ); + +Now, we can replace the usage of the static method and move all the validation +rules to ``validation.yml``: + +.. code-block:: yaml + + # validation.yml + Post: + properties: + title: + - NotNull: ~ + - NotBlank: ~ + body: + - Min: 100 diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..d1a851d --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,19 @@ +The Book +======== + +.. toctree:: + :maxdepth: 1 + + intro + usage + middlewares + organizing_controllers + services + providers + testing + cookbook/index + internals + contributing + providers/index + web_servers + changelog diff --git a/doc/internals.rst b/doc/internals.rst new file mode 100644 index 0000000..c7ffac8 --- /dev/null +++ b/doc/internals.rst @@ -0,0 +1,84 @@ +Internals +========= + +This chapter will tell you how Silex works internally. + +Silex +----- + +Application +~~~~~~~~~~~ + +The application is the main interface to Silex. It implements Symfony's +`HttpKernelInterface +`_, +so you can pass a `Request +`_ +to the ``handle`` method and it will return a `Response +`_. + +It extends the ``Pimple`` service container, allowing for flexibility on the +outside as well as the inside. You could replace any service, and you are also +able to read them. + +The application makes strong use of the `EventDispatcher +`_ to hook into the Symfony `HttpKernel +`_ +events. This allows fetching the ``Request``, converting string responses into +``Response`` objects and handling Exceptions. We also use it to dispatch some +custom events like before/after middlewares and errors. + +Controller +~~~~~~~~~~ + +The Symfony `Route +`_ is +actually quite powerful. Routes can be named, which allows for URL generation. +They can also have requirements for the variable parts. In order to allow +setting these through a nice interface, the ``match`` method (which is used by +``get``, ``post``, etc.) returns an instance of the ``Controller``, which +wraps a route. + +ControllerCollection +~~~~~~~~~~~~~~~~~~~~ + +One of the goals of exposing the `RouteCollection +`_ +was to make it mutable, so providers could add stuff to it. The challenge here +is the fact that routes know nothing about their name. The name only has +meaning in context of the ``RouteCollection`` and cannot be changed. + +To solve this challenge we came up with a staging area for routes. The +``ControllerCollection`` holds the controllers until ``flush`` is called, at +which point the routes are added to the ``RouteCollection``. Also, the +controllers are then frozen. This means that they can no longer be modified +and will throw an Exception if you try to do so. + +Unfortunately no good way for flushing implicitly could be found, which is why +flushing is now always explicit. The Application will flush, but if you want +to read the ``ControllerCollection`` before the request takes place, you will +have to call flush yourself. + +The ``Application`` provides a shortcut ``flush`` method for flushing the +``ControllerCollection``. + +.. tip:: + + Instead of creating an instance of ``RouteCollection`` yourself, use the + ``$app['controllers_factory']`` factory instead. + +Symfony +------- + +Following Symfony components are used by Silex: + +* **HttpFoundation**: For ``Request`` and ``Response``. + +* **HttpKernel**: Because we need a heart. + +* **Routing**: For matching defined routes. + +* **EventDispatcher**: For hooking into the HttpKernel. + +For more information, `check out the Symfony website `_. diff --git a/doc/intro.rst b/doc/intro.rst new file mode 100644 index 0000000..2ab2bc3 --- /dev/null +++ b/doc/intro.rst @@ -0,0 +1,50 @@ +Introduction +============ + +Silex is a PHP microframework. It is built on the shoulders of `Symfony`_ and +`Pimple`_ and also inspired by `Sinatra`_. + +Silex aims to be: + +* *Concise*: Silex exposes an intuitive and concise API. + +* *Extensible*: Silex has an extension system based around the Pimple + service-container that makes it easy to tie in third party libraries. + +* *Testable*: Silex uses Symfony's HttpKernel which abstracts request and + response. This makes it very easy to test apps and the framework itself. It + also respects the HTTP specification and encourages its proper use. + +In a nutshell, you define controllers and map them to routes, all in one step. + +Usage +----- + +.. code-block:: php + + get('/hello/{name}', function ($name) use ($app) { + return 'Hello '.$app->escape($name); + }); + + $app->run(); + +All that is needed to get access to the Framework is to include the +autoloader. + +Next, a route for ``/hello/{name}`` that matches for ``GET`` requests is +defined. When the route matches, the function is executed and the return value +is sent back to the client. + +Finally, the app is run. Visit ``/hello/world`` to see the result. It's really +that easy! + +.. _Symfony: http://symfony.com/ +.. _Pimple: http://pimple.sensiolabs.org/ +.. _Sinatra: http://www.sinatrarb.com/ diff --git a/doc/middlewares.rst b/doc/middlewares.rst new file mode 100644 index 0000000..c5c17cf --- /dev/null +++ b/doc/middlewares.rst @@ -0,0 +1,162 @@ +Middleware +========== + +Silex allows you to run code, that changes the default Silex behavior, at +different stages during the handling of a request through *middleware*: + +* *Application middleware* is triggered independently of the current handled + request; + +* *Route middleware* is triggered when its associated route is matched. + +Application Middleware +---------------------- + +Application middleware is only run for the "master" Request. + +Before Middleware +~~~~~~~~~~~~~~~~~ + +A *before* application middleware allows you to tweak the Request before the +controller is executed:: + + $app->before(function (Request $request, Application $app) { + // ... + }); + +By default, the middleware is run after the routing and the security. + +If you want your middleware to be run even if an exception is thrown early on +(on a 404 or 403 error for instance), then, you need to register it as an +early event:: + + $app->before(function (Request $request, Application $app) { + // ... + }, Application::EARLY_EVENT); + +In this case, the routing and the security won't have been executed, and so you +won't have access to the locale, the current route, or the security user. + +.. note:: + + The before middleware is an event registered on the Symfony *request* + event. + +After Middleware +~~~~~~~~~~~~~~~~ + +An *after* application middleware allows you to tweak the Response before it +is sent to the client:: + + $app->after(function (Request $request, Response $response) { + // ... + }); + +.. note:: + + The after middleware is an event registered on the Symfony *response* + event. + +Finish Middleware +~~~~~~~~~~~~~~~~~ + +A *finish* application middleware allows you to execute tasks after the +Response has been sent to the client (like sending emails or logging):: + + $app->finish(function (Request $request, Response $response) { + // ... + // Warning: modifications to the Request or Response will be ignored + }); + +.. note:: + + The finish middleware is an event registered on the Symfony *terminate* + event. + +Route Middleware +---------------- + +Route middleware is added to routes or route collections and it is only +triggered when the corresponding route is matched. You can also stack them:: + + $app->get('/somewhere', function () { + // ... + }) + ->before($before1) + ->before($before2) + ->after($after1) + ->after($after2) + ; + +Before Middleware +~~~~~~~~~~~~~~~~~ + +A *before* route middleware is fired just before the route callback, but after +the *before* application middleware:: + + $before = function (Request $request, Application $app) { + // ... + }; + + $app->get('/somewhere', function () { + // ... + }) + ->before($before); + +After Middleware +~~~~~~~~~~~~~~~~ + +An *after* route middleware is fired just after the route callback, but before +the application *after* application middleware:: + + $after = function (Request $request, Response $response, Application $app) { + // ... + }; + + $app->get('/somewhere', function () { + // ... + }) + ->after($after); + +Middleware Priority +------------------- + +You can add as much middleware as you want, in which case they are triggered +in the same order as you added them. + +You can explicitly control the priority of your middleware by passing an +additional argument to the registration methods:: + + $app->before(function (Request $request) { + // ... + }, 32); + +As a convenience, two constants allow you to register an event as early as +possible or as late as possible:: + + $app->before(function (Request $request) { + // ... + }, Application::EARLY_EVENT); + + $app->before(function (Request $request) { + // ... + }, Application::LATE_EVENT); + +Short-circuiting the Controller +------------------------------- + +If a *before* middleware returns a ``Response`` object, the request handling is +short-circuited (the next middleware won't be run, nor the route +callback), and the Response is passed to the *after* middleware right away:: + + $app->before(function (Request $request) { + // redirect the user to the login screen if access to the Resource is protected + if (...) { + return new RedirectResponse('/login'); + } + }); + +.. note:: + + A ``RuntimeException`` is thrown if a before middleware does not return a + Response or ``null``. diff --git a/doc/organizing_controllers.rst b/doc/organizing_controllers.rst new file mode 100644 index 0000000..50558cb --- /dev/null +++ b/doc/organizing_controllers.rst @@ -0,0 +1,84 @@ +Organizing Controllers +====================== + +When your application starts to define too many controllers, you might want to +group them logically:: + + // define controllers for a blog + $blog = $app['controllers_factory']; + $blog->get('/', function () { + return 'Blog home page'; + }); + // ... + + // define controllers for a forum + $forum = $app['controllers_factory']; + $forum->get('/', function () { + return 'Forum home page'; + }); + + // define "global" controllers + $app->get('/', function () { + return 'Main home page'; + }); + + $app->mount('/blog', $blog); + $app->mount('/forum', $forum); + + // define controllers for a admin + $app->mount('/admin', function ($admin) { + // recursively mount + $admin->mount('/blog', function ($user) { + $user->get('/', function () { + return 'Admin Blog home page'; + }); + }); + }); + +.. note:: + + ``$app['controllers_factory']`` is a factory that returns a new instance + of ``ControllerCollection`` when used. + +``mount()`` prefixes all routes with the given prefix and merges them into the +main Application. So, ``/`` will map to the main home page, ``/blog/`` to the +blog home page, ``/forum/`` to the forum home page, and ``/admin/blog/`` to the +admin blog home page. + +.. caution:: + + When mounting a route collection under ``/blog``, it is not possible to + define a route for the ``/blog`` URL. The shortest possible URL is + ``/blog/``. + +.. note:: + + When calling ``get()``, ``match()``, or any other HTTP methods on the + Application, you are in fact calling them on a default instance of + ``ControllerCollection`` (stored in ``$app['controllers']``). + +Another benefit is the ability to apply settings on a set of controllers very +easily. Building on the example from the middleware section, here is how you +would secure all controllers for the backend collection:: + + $backend = $app['controllers_factory']; + + // ensure that all controllers require logged-in users + $backend->before($mustBeLogged); + +.. tip:: + + For a better readability, you can split each controller collection into a + separate file:: + + // blog.php + $blog = $app['controllers_factory']; + $blog->get('/', function () { return 'Blog home page'; }); + + return $blog; + + // app.php + $app->mount('/blog', include 'blog.php'); + + Instead of requiring a file, you can also create a :ref:`Controller + provider `. diff --git a/doc/providers.rst b/doc/providers.rst new file mode 100644 index 0000000..a53fdc9 --- /dev/null +++ b/doc/providers.rst @@ -0,0 +1,256 @@ +Providers +========= + +Providers allow the developer to reuse parts of an application into another +one. Silex provides two types of providers defined by two interfaces: +``ServiceProviderInterface`` for services and ``ControllerProviderInterface`` +for controllers. + +Service Providers +----------------- + +Loading providers +~~~~~~~~~~~~~~~~~ + +In order to load and use a service provider, you must register it on the +application:: + + $app = new Silex\Application(); + + $app->register(new Acme\DatabaseServiceProvider()); + +You can also provide some parameters as a second argument. These will be set +**after** the provider is registered, but **before** it is booted:: + + $app->register(new Acme\DatabaseServiceProvider(), array( + 'database.dsn' => 'mysql:host=localhost;dbname=myapp', + 'database.user' => 'root', + 'database.password' => 'secret_root_password', + )); + +Conventions +~~~~~~~~~~~ + +You need to watch out in what order you do certain things when interacting +with providers. Just keep these rules in mind: + +* Overriding existing services must occur **after** the provider is + registered. + + *Reason: If the service already exists, the provider will overwrite it.* + +* You can set parameters any time **after** the provider is registered, but + **before** the service is accessed. + + *Reason: Providers can set default values for parameters. Just like with + services, the provider will overwrite existing values.* + +Included providers +~~~~~~~~~~~~~~~~~~ + +There are a few providers that you get out of the box. All of these are within +the ``Silex\Provider`` namespace: + +* :doc:`DoctrineServiceProvider ` +* :doc:`FormServiceProvider ` +* :doc:`HttpCacheServiceProvider ` +* :doc:`MonologServiceProvider ` +* :doc:`RememberMeServiceProvider ` +* :doc:`SecurityServiceProvider ` +* :doc:`SerializerServiceProvider ` +* :doc:`ServiceControllerServiceProvider ` +* :doc:`SessionServiceProvider ` +* :doc:`SwiftmailerServiceProvider ` +* :doc:`TranslationServiceProvider ` +* :doc:`TwigServiceProvider ` +* :doc:`ValidatorServiceProvider ` + +.. note:: + + The Silex core team maintains a `WebProfiler + `_ provider that helps debug + code in the development environment thanks to the Symfony web debug toolbar + and the Symfony profiler. + +Third party providers +~~~~~~~~~~~~~~~~~~~~~ + +Some service providers are developed by the community. Those third-party +providers are listed on `Silex' repository wiki +`_. + +You are encouraged to share yours. + +Creating a provider +~~~~~~~~~~~~~~~~~~~ + +Providers must implement the ``Pimple\ServiceProviderInterface``:: + + interface ServiceProviderInterface + { + public function register(Container $container); + } + +This is very straight forward, just create a new class that implements the +register method. In the ``register()`` method, you can define services on the +application which then may make use of other services and parameters. + +.. tip:: + + The ``Pimple\ServiceProviderInterface`` belongs to the Pimple package, so + take care to only use the API of ``Pimple\Container`` within your + ``register`` method. Not only is this a good practice due to the way Pimple + and Silex work, but may allow your provider to be used outside of Silex. + +Optionally, your service provider can implement the +``Silex\Api\BootableProviderInterface``. A bootable provider must +implement the ``boot()`` method, with which you can configure the application, just +before it handles a request:: + + interface BootableProviderInterface + { + function boot(Application $app); + } + +Another optional interface, is the ``Silex\Api\EventListenerProviderInterface``. +This interface contains the ``subscribe()`` method, which allows your provider to +subscribe event listener with Silex's EventDispatcher, just before it handles a +request:: + + interface EventListenerProviderInterface + { + function subscribe(Container $app, EventDispatcherInterface $dispatcher); + } + +Here is an example of such a provider:: + + namespace Acme; + + use Pimple\Container; + use Pimple\ServiceProviderInterface; + use Silex\Application; + use Silex\Api\BootableProviderInterface; + use Silex\Api\EventListenerProviderInterface; + use Symfony\Component\HttpKernel\KernelEvents; + use Symfony\Component\HttpKernel\Event\FilterResponseEvent; + + class HelloServiceProvider implements ServiceProviderInterface, BootableProviderInterface, EventListenerProviderInterface + { + public function register(Container $app) + { + $app['hello'] = $app->protect(function ($name) use ($app) { + $default = $app['hello.default_name'] ? $app['hello.default_name'] : ''; + $name = $name ?: $default; + + return 'Hello '.$app->escape($name); + }); + } + + public function boot(Application $app) + { + // do something + } + + public function subscribe(Container $app, EventDispatcherInterface $dispatcher) + { + $dispatcher->addListener(KernelEvents::REQUEST, function(FilterResponseEvent $event) use ($app) { + // do something + }); + } + } + +This class provides a ``hello`` service which is a protected closure. It takes +a ``name`` argument and will return ``hello.default_name`` if no name is +given. If the default is also missing, it will use an empty string. + +You can now use this provider as follows:: + + use Symfony\Component\HttpFoundation\Request; + + $app = new Silex\Application(); + + $app->register(new Acme\HelloServiceProvider(), array( + 'hello.default_name' => 'Igor', + )); + + $app->get('/hello', function (Request $request) use ($app) { + $name = $request->get('name'); + + return $app['hello']($name); + }); + +In this example we are getting the ``name`` parameter from the query string, +so the request path would have to be ``/hello?name=Fabien``. + +.. _controller-providers: + +Controller Providers +-------------------- + +Loading providers +~~~~~~~~~~~~~~~~~ + +In order to load and use a controller provider, you must "mount" its +controllers under a path:: + + $app = new Silex\Application(); + + $app->mount('/blog', new Acme\BlogControllerProvider()); + +All controllers defined by the provider will now be available under the +``/blog`` path. + +Creating a provider +~~~~~~~~~~~~~~~~~~~ + +Providers must implement the ``Silex\Api\ControllerProviderInterface``:: + + interface ControllerProviderInterface + { + public function connect(Application $app); + } + +Here is an example of such a provider:: + + namespace Acme; + + use Silex\Application; + use Silex\Api\ControllerProviderInterface; + + class HelloControllerProvider implements ControllerProviderInterface + { + public function connect(Application $app) + { + // creates a new controller based on the default route + $controllers = $app['controllers_factory']; + + $controllers->get('/', function (Application $app) { + return $app->redirect('/hello'); + }); + + return $controllers; + } + } + +The ``connect`` method must return an instance of ``ControllerCollection``. +``ControllerCollection`` is the class where all controller related methods are +defined (like ``get``, ``post``, ``match``, ...). + +.. tip:: + + The ``Application`` class acts in fact as a proxy for these methods. + +You can use this provider as follows:: + + $app = new Silex\Application(); + + $app->mount('/blog', new Acme\HelloControllerProvider()); + +In this example, the ``/blog/`` path now references the controller defined in +the provider. + +.. tip:: + + You can also define a provider that implements both the service and the + controller provider interface and package in the same class the services + needed to make your controllers work. diff --git a/doc/providers/asset.rst b/doc/providers/asset.rst new file mode 100644 index 0000000..ef084e5 --- /dev/null +++ b/doc/providers/asset.rst @@ -0,0 +1,60 @@ +Asset +===== + +The *AssetServiceProvider* provides a way to manage URL generation and +versioning of web assets such as CSS stylesheets, JavaScript files and image +files. + +Parameters +---------- + +* **assets.version**: Default version for assets. + +* **assets.format_version** (optional): Default format for assets. + +* **assets.named_packages** (optional): Named packages. Keys are the package + names and values the configuration (supported keys are ``version``, + ``version_format``, ``base_urls``, and ``base_path``). + +Services +-------- + +* **assets.packages**: The asset service. + +Registering +----------- + +.. code-block:: php + + $app->register(new Silex\Provider\AssetServiceProvider(), array( + 'assets.version' => 'v1', + 'assets.version_format' => '%s?version=%s', + 'assets.named_packages' => array( + 'css' => array('version' => 'css2', 'base_path' => '/whatever-makes-sense'), + 'images' => array('base_urls' => array('https://img.example.com')), + ), + )); + +.. note:: + + Add the Symfony Asset Component as a dependency: + + .. code-block:: bash + + composer require symfony/asset + +Usage +----- + +The AssetServiceProvider is mostly useful with the Twig provider: + +.. code-block:: jinja + + {{ asset('/css/foo.png') }} + {{ asset('/css/foo.css', 'css') }} + {{ asset('/img/foo.png', 'images') }} + + {{ asset_version('/css/foo.png') }} + +For more information, check out the `Asset Component documentation +`_. diff --git a/doc/providers/csrf.rst b/doc/providers/csrf.rst new file mode 100644 index 0000000..3bd35f4 --- /dev/null +++ b/doc/providers/csrf.rst @@ -0,0 +1,52 @@ +CSRF +==== + +The *CsrfServiceProvider* provides a service for building forms in your +application with the Symfony Form component. + +Parameters +---------- + +* none + +Services +-------- + +* **csrf.token_manager**: An instance of an implementation of the + `CsrfProviderInterface + `_, + defaults to a `DefaultCsrfProvider + `_. + +Registering +----------- + +.. code-block:: php + + use Silex\Provider\CsrfServiceProvider; + + $app->register(new CsrfServiceProvider()); + +.. note:: + + Add the Symfony's `Security CSRF Component + `_ as a + dependency: + + .. code-block:: bash + + composer require symfony/security-csrf + +Usage +----- + +When the CSRF Service Provider is registered, all forms created via the Form +Service Provider are protected against CSRF by default. + +You can also use the CSRF protection even without using the Symfony Form +component. If, for example, you're doing a DELETE action, you can check the +CSRF token:: + + use Symfony\Component\Security\Csrf\CsrfToken; + + $app['csrf.token_manager']->isTokenValid(new CsrfToken('token_id', 'TOKEN')); diff --git a/doc/providers/doctrine.rst b/doc/providers/doctrine.rst new file mode 100644 index 0000000..0ef167b --- /dev/null +++ b/doc/providers/doctrine.rst @@ -0,0 +1,137 @@ +Doctrine +======== + +The *DoctrineServiceProvider* provides integration with the `Doctrine DBAL +`_ for easy database access +(Doctrine ORM integration is **not** supplied). + +Parameters +---------- + +* **db.options**: Array of Doctrine DBAL options. + + These options are available: + + * **driver**: The database driver to use, defaults to ``pdo_mysql``. + Can be any of: ``pdo_mysql``, ``pdo_sqlite``, ``pdo_pgsql``, + ``pdo_oci``, ``oci8``, ``ibm_db2``, ``pdo_ibm``, ``pdo_sqlsrv``. + + * **dbname**: The name of the database to connect to. + + * **host**: The host of the database to connect to. Defaults to + localhost. + + * **user**: The user of the database to connect to. Defaults to + root. + + * **password**: The password of the database to connect to. + + * **charset**: Only relevant for ``pdo_mysql``, and ``pdo_oci/oci8``, + specifies the charset used when connecting to the database. + + * **path**: Only relevant for ``pdo_sqlite``, specifies the path to + the SQLite database. + + * **port**: Only relevant for ``pdo_mysql``, ``pdo_pgsql``, and ``pdo_oci/oci8``, + specifies the port of the database to connect to. + + These and additional options are described in detail in the `Doctrine DBAL + configuration documentation `_. + +Services +-------- + +* **db**: The database connection, instance of + ``Doctrine\DBAL\Connection``. + +* **db.config**: Configuration object for Doctrine. Defaults to + an empty ``Doctrine\DBAL\Configuration``. + +* **db.event_manager**: Event Manager for Doctrine. + +Registering +----------- + +.. code-block:: php + + $app->register(new Silex\Provider\DoctrineServiceProvider(), array( + 'db.options' => array( + 'driver' => 'pdo_sqlite', + 'path' => __DIR__.'/app.db', + ), + )); + +.. note:: + + Add the Doctrine DBAL as a dependency: + + .. code-block:: bash + + composer require "doctrine/dbal:~2.2" + +Usage +----- + +The Doctrine provider provides a ``db`` service. Here is a usage +example:: + + $app->get('/blog/{id}', function ($id) use ($app) { + $sql = "SELECT * FROM posts WHERE id = ?"; + $post = $app['db']->fetchAssoc($sql, array((int) $id)); + + return "

{$post['title']}

". + "

{$post['body']}

"; + }); + +Using multiple databases +------------------------ + +The Doctrine provider can allow access to multiple databases. In order to +configure the data sources, replace the **db.options** with **dbs.options**. +**dbs.options** is an array of configurations where keys are connection names +and values are options:: + + $app->register(new Silex\Provider\DoctrineServiceProvider(), array( + 'dbs.options' => array ( + 'mysql_read' => array( + 'driver' => 'pdo_mysql', + 'host' => 'mysql_read.someplace.tld', + 'dbname' => 'my_database', + 'user' => 'my_username', + 'password' => 'my_password', + 'charset' => 'utf8mb4', + ), + 'mysql_write' => array( + 'driver' => 'pdo_mysql', + 'host' => 'mysql_write.someplace.tld', + 'dbname' => 'my_database', + 'user' => 'my_username', + 'password' => 'my_password', + 'charset' => 'utf8mb4', + ), + ), + )); + +The first registered connection is the default and can simply be accessed as +you would if there was only one connection. Given the above configuration, +these two lines are equivalent:: + + $app['db']->fetchAll('SELECT * FROM table'); + + $app['dbs']['mysql_read']->fetchAll('SELECT * FROM table'); + +Using multiple connections:: + + $app->get('/blog/{id}', function ($id) use ($app) { + $sql = "SELECT * FROM posts WHERE id = ?"; + $post = $app['dbs']['mysql_read']->fetchAssoc($sql, array((int) $id)); + + $sql = "UPDATE posts SET value = ? WHERE id = ?"; + $app['dbs']['mysql_write']->executeUpdate($sql, array('newValue', (int) $id)); + + return "

{$post['title']}

". + "

{$post['body']}

"; + }); + +For more information, consult the `Doctrine DBAL documentation +`_. diff --git a/doc/providers/form.rst b/doc/providers/form.rst new file mode 100644 index 0000000..6360007 --- /dev/null +++ b/doc/providers/form.rst @@ -0,0 +1,204 @@ +Form +==== + +The *FormServiceProvider* provides a service for building forms in +your application with the Symfony Form component. + +Parameters +---------- + +* none + +Services +-------- + +* **form.factory**: An instance of `FormFactory + `_, + that is used to build a form. + +Registering +----------- + +.. code-block:: php + + use Silex\Provider\FormServiceProvider; + + $app->register(new FormServiceProvider()); + +.. note:: + + If you don't want to create your own form layout, it's fine: a default one + will be used. But you will have to register the :doc:`translation provider + ` as the default form layout requires it:: + + $app->register(new Silex\Provider\TranslationServiceProvider(), array( + 'translator.domains' => array(), + )); + + If you want to use validation with forms, do not forget to register the + :doc:`Validator provider `. + +.. note:: + + Add the Symfony Form Component as a dependency: + + .. code-block:: bash + + composer require symfony/form + + If you are going to use the validation extension with forms, you must also + add a dependency to the ``symfony/config`` and ``symfony/translation`` + components: + + .. code-block:: bash + + composer require symfony/validator symfony/config symfony/translation + + If you want to use forms in your Twig templates, you can also install the + Symfony Twig Bridge. Make sure to install, if you didn't do that already, + the Translation component in order for the bridge to work: + + .. code-block:: bash + + composer require symfony/twig-bridge symfony/translation + +Usage +----- + +The FormServiceProvider provides a ``form.factory`` service. Here is a usage +example:: + + use Symfony\Component\Form\Extension\Core\Type\FormType; + use Symfony\Component\Form\Extension\Core\Type\ChoiceType; + + $app->match('/form', function (Request $request) use ($app) { + // some default data for when the form is displayed the first time + $data = array( + 'name' => 'Your name', + 'email' => 'Your email', + ); + + $form = $app['form.factory']->createBuilder(FormType::class, $data) + ->add('name') + ->add('email') + ->add('billing_plan', ChoiceType::class, array( + 'choices' => array(1 => 'free', 2 => 'small_business', 3 => 'corporate'), + 'expanded' => true, + )) + ->getForm(); + + $form->handleRequest($request); + + if ($form->isValid()) { + $data = $form->getData(); + + // do something with the data + + // redirect somewhere + return $app->redirect('...'); + } + + // display the form + return $app['twig']->render('index.twig', array('form' => $form->createView())); + }); + +And here is the ``index.twig`` form template (requires ``symfony/twig-bridge``): + +.. code-block:: jinja + +
+ {{ form_widget(form) }} + + +
+ +If you are using the validator provider, you can also add validation to your +form by adding constraints on the fields:: + + use Symfony\Component\Form\Extension\Core\Type\FormType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\Form\Extension\Core\Type\ChoiceType; + use Symfony\Component\Validator\Constraints as Assert; + + $app->register(new Silex\Provider\ValidatorServiceProvider()); + $app->register(new Silex\Provider\TranslationServiceProvider(), array( + 'translator.domains' => array(), + )); + + $form = $app['form.factory']->createBuilder(FormType::class) + ->add('name', TextType::class, array( + 'constraints' => array(new Assert\NotBlank(), new Assert\Length(array('min' => 5))) + )) + ->add('email', TextType::class, array( + 'constraints' => new Assert\Email() + )) + ->add('billing_plan', ChoiceType::class, array( + 'choices' => array(1 => 'free', 2 => 'small_business', 3 => 'corporate'), + 'expanded' => true, + 'constraints' => new Assert\Choice(array(1, 2, 3)), + )) + ->getForm(); + +You can register form types by extending ``form.types``:: + + $app['your.type.service'] = function ($app) { + return new YourServiceFormType(); + }; + $app->extend('form.types', function ($types) use ($app) { + $types[] = new YourFormType(); + $types[] = 'your.type.service'; + + return $types; + })); + +You can register form extensions by extending ``form.extensions``:: + + $app->extend('form.extensions', function ($extensions) use ($app) { + $extensions[] = new YourTopFormExtension(); + + return $extensions; + }); + + +You can register form type extensions by extending ``form.type.extensions``:: + + $app['your.type.extension.service'] = function ($app) { + return new YourServiceFormTypeExtension(); + }; + $app->extend('form.type.extensions', function ($extensions) use ($app) { + $extensions[] = new YourFormTypeExtension(); + $extensions[] = 'your.type.extension.service'; + + return $extensions; + }); + +You can register form type guessers by extending ``form.type.guessers``:: + + $app['your.type.guesser.service'] = function ($app) { + return new YourServiceFormTypeGuesser(); + }; + $app->extend('form.type.guessers', function ($guessers) use ($app) { + $guessers[] = new YourFormTypeGuesser(); + $guessers[] = 'your.type.guesser.service'; + + return $guessers; + }); + +.. warning:: + + CSRF protection is only available and automatically enabled when the + :doc:`CSRF Service Provider
` is registered. + +Traits +------ + +``Silex\Application\FormTrait`` adds the following shortcuts: + +* **form**: Creates a FormBuilder instance. + +.. code-block:: php + + $app->form($data); + +For more information, consult the `Symfony Forms documentation +`_. diff --git a/doc/providers/http_cache.rst b/doc/providers/http_cache.rst new file mode 100644 index 0000000..8bc98f6 --- /dev/null +++ b/doc/providers/http_cache.rst @@ -0,0 +1,128 @@ +HTTP Cache +========== + +The *HttpCacheServiceProvider* provides support for the Symfony Reverse +Proxy. + +Parameters +---------- + +* **http_cache.cache_dir**: The cache directory to store the HTTP cache data. + +* **http_cache.options** (optional): An array of options for the `HttpCache + `_ + constructor. + +Services +-------- + +* **http_cache**: An instance of `HttpCache + `_. + +* **http_cache.esi**: An instance of `Esi + `_, + that implements the ESI capabilities to Request and Response instances. + +* **http_cache.store**: An instance of `Store + `_, + that implements all the logic for storing cache metadata (Request and Response + headers). + +Registering +----------- + +.. code-block:: php + + $app->register(new Silex\Provider\HttpCacheServiceProvider(), array( + 'http_cache.cache_dir' => __DIR__.'/cache/', + )); + +Usage +----- + +Silex already supports any reverse proxy like Varnish out of the box by +setting Response HTTP cache headers:: + + use Symfony\Component\HttpFoundation\Response; + + $app->get('/', function() { + return new Response('Foo', 200, array( + 'Cache-Control' => 's-maxage=5', + )); + }); + +.. tip:: + + If you want Silex to trust the ``X-Forwarded-For*`` headers from your + reverse proxy at address $ip, you will need to whitelist it as documented + in `Trusting Proxies + `_. + + If you would be running Varnish in front of your application on the same machine:: + + use Symfony\Component\HttpFoundation\Request; + + Request::setTrustedProxies(array('127.0.0.1', '::1')); + $app->run(); + +This provider allows you to use the Symfony reverse proxy natively with +Silex applications by using the ``http_cache`` service. The Symfony reverse proxy +acts much like any other proxy would, so you will want to whitelist it:: + + use Symfony\Component\HttpFoundation\Request; + + Request::setTrustedProxies(array('127.0.0.1')); + $app['http_cache']->run(); + +The provider also provides ESI support:: + + $app->get('/', function() { + $response = new Response(<< + + Hello + + + + + EOF + , 200, array( + 'Surrogate-Control' => 'content="ESI/1.0"', + )); + + $response->setTtl(20); + + return $response; + }); + + $app->get('/included', function() { + $response = new Response('Foo'); + $response->setTtl(5); + + return $response; + }); + + $app['http_cache']->run(); + +If your application doesn't use ESI, you can disable it to slightly improve the +overall performance:: + + $app->register(new Silex\Provider\HttpCacheServiceProvider(), array( + 'http_cache.cache_dir' => __DIR__.'/cache/', + 'http_cache.esi' => null, + )); + +.. tip:: + + To help you debug caching issues, set your application ``debug`` to true. + Symfony automatically adds a ``X-Symfony-Cache`` header to each response + with useful information about cache hits and misses. + + If you are *not* using the Symfony Session provider, you might want to set + the PHP ``session.cache_limiter`` setting to an empty value to avoid the + default PHP behavior. + + Finally, check that your Web server does not override your caching strategy. + +For more information, consult the `Symfony HTTP Cache documentation +`_. diff --git a/doc/providers/http_fragment.rst b/doc/providers/http_fragment.rst new file mode 100644 index 0000000..8e68185 --- /dev/null +++ b/doc/providers/http_fragment.rst @@ -0,0 +1,70 @@ +HTTP Fragment +============= + +The *HttpFragmentServiceProvider* provides support for the Symfony fragment +sub-framework, which allows you to embed fragments of HTML in a template. + +Parameters +---------- + +* **fragment.path**: The path to use for the URL generated for ESI and + HInclude URLs (``/_fragment`` by default). + +* **uri_signer.secret**: The secret to use for the URI signer service (used + for the HInclude renderer). + +* **fragment.renderers.hinclude.global_template**: The content or Twig + template to use for the default content when using the HInclude renderer. + +Services +-------- + +* **fragment.handler**: An instance of `FragmentHandler + `_. + +* **fragment.renderers**: An array of fragment renderers (by default, the + inline, ESI, and HInclude renderers are pre-configured). + +Registering +----------- + +.. code-block:: php + + $app->register(new Silex\Provider\HttpFragmentServiceProvider()); + +Usage +----- + +.. note:: + + This section assumes that you are using Twig for your templates. + +Instead of building a page out of a single request/controller/template, the +fragment framework allows you to build a page from several +controllers/sub-requests/sub-templates by using **fragments**. + +Including "sub-pages" in the main page can be done with the Twig ``render()`` +function: + +.. code-block:: jinja + + The main page content. + + {{ render('/foo') }} + + The main page content resumes here. + +The ``render()`` call is replaced by the content of the ``/foo`` URL +(internally, a sub-request is handled by Silex to render the sub-page). + +Instead of making internal sub-requests, you can also use the ESI (the +sub-request is handled by a reverse proxy) or the HInclude strategies (the +sub-request is handled by a web browser): + +.. code-block:: jinja + + {{ render(url('route_name')) }} + + {{ render_esi(url('route_name')) }} + + {{ render_hinclude(url('route_name')) }} diff --git a/doc/providers/index.rst b/doc/providers/index.rst new file mode 100644 index 0000000..8c5a175 --- /dev/null +++ b/doc/providers/index.rst @@ -0,0 +1,24 @@ +Built-in Service Providers +========================== + +.. toctree:: + :maxdepth: 1 + + twig + asset + monolog + session + swiftmailer + locale + translation + validator + form + csrf + http_cache + http_fragment + security + remember_me + serializer + service_controller + var_dumper + doctrine diff --git a/doc/providers/locale.rst b/doc/providers/locale.rst new file mode 100644 index 0000000..8f6cd67 --- /dev/null +++ b/doc/providers/locale.rst @@ -0,0 +1,24 @@ +Locale +====== + +The *LocaleServiceProvider* manages the locale of an application. + +Parameters +---------- + +* **locale**: The locale of the user. When set before any request handling, it + defines the default locale (``en`` by default). When a request is being + handled, it is automatically set according to the ``_locale`` request + attribute of the current route. + +Services +-------- + +* n/a + +Registering +----------- + +.. code-block:: php + + $app->register(new Silex\Provider\LocaleServiceProvider()); diff --git a/doc/providers/monolog.rst b/doc/providers/monolog.rst new file mode 100644 index 0000000..8a83127 --- /dev/null +++ b/doc/providers/monolog.rst @@ -0,0 +1,115 @@ +Monolog +======= + +The *MonologServiceProvider* provides a default logging mechanism through +Jordi Boggiano's `Monolog `_ library. + +It will log requests and errors and allow you to add logging to your +application. This allows you to debug and monitor the behaviour, +even in production. + +Parameters +---------- + +* **monolog.logfile**: File where logs are written to. +* **monolog.bubble**: (optional) Whether the messages that are handled can bubble up the stack or not. +* **monolog.permission**: (optional) File permissions default (null), nothing change. + +* **monolog.level** (optional): Level of logging, defaults + to ``DEBUG``. Must be one of ``Logger::DEBUG``, ``Logger::INFO``, + ``Logger::WARNING``, ``Logger::ERROR``. ``DEBUG`` will log + everything, ``INFO`` will log everything except ``DEBUG``, + etc. + + In addition to the ``Logger::`` constants, it is also possible to supply the + level in string form, for example: ``"DEBUG"``, ``"INFO"``, ``"WARNING"``, + ``"ERROR"``. + +* **monolog.name** (optional): Name of the monolog channel, + defaults to ``myapp``. + +* **monolog.exception.logger_filter** (optional): An anonymous function that + filters which exceptions should be logged. + +* **monolog.use_error_handler** (optional): Whether errors and uncaught exceptions + should be handled by the Monolog ``ErrorHandler`` class and added to the log. + By default the error handler is enabled unless the application ``debug`` parameter + is set to true. + + Please note that enabling the error handler may silence some errors, + ignoring the PHP ``display_errors`` configuration setting. + +Services +-------- + +* **monolog**: The monolog logger instance. + + Example usage:: + + $app['monolog']->addDebug('Testing the Monolog logging.'); + +* **monolog.listener**: An event listener to log requests, responses and errors. + +Registering +----------- + +.. code-block:: php + + $app->register(new Silex\Provider\MonologServiceProvider(), array( + 'monolog.logfile' => __DIR__.'/development.log', + )); + +.. note:: + + Add Monolog as a dependency: + + .. code-block:: bash + + composer require monolog/monolog + +Usage +----- + +The MonologServiceProvider provides a ``monolog`` service. You can use it to +add log entries for any logging level through ``addDebug()``, ``addInfo()``, +``addWarning()`` and ``addError()``:: + + use Symfony\Component\HttpFoundation\Response; + + $app->post('/user', function () use ($app) { + // ... + + $app['monolog']->addInfo(sprintf("User '%s' registered.", $username)); + + return new Response('', 201); + }); + +Customization +------------- + +You can configure Monolog (like adding or changing the handlers) before using +it by extending the ``monolog`` service:: + + $app->extend('monolog', function($monolog, $app) { + $monolog->pushHandler(...); + + return $monolog; + }); + +By default, all requests, responses and errors are logged by an event listener +registered as a service called `monolog.listener`. You can replace or remove +this service if you want to modify or disable the logged information. + +Traits +------ + +``Silex\Application\MonologTrait`` adds the following shortcuts: + +* **log**: Logs a message. + +.. code-block:: php + + $app->log(sprintf("User '%s' registered.", $username)); + +For more information, check out the `Monolog documentation +`_. diff --git a/doc/providers/remember_me.rst b/doc/providers/remember_me.rst new file mode 100644 index 0000000..7fdaaab --- /dev/null +++ b/doc/providers/remember_me.rst @@ -0,0 +1,69 @@ +Remember Me +=========== + +The *RememberMeServiceProvider* adds "Remember-Me" authentication to the +*SecurityServiceProvider*. + +Parameters +---------- + +n/a + +Services +-------- + +n/a + +.. note:: + + The service provider defines many other services that are used internally + but rarely need to be customized. + +Registering +----------- + +Before registering this service provider, you must register the +*SecurityServiceProvider*:: + + $app->register(new Silex\Provider\SecurityServiceProvider()); + $app->register(new Silex\Provider\RememberMeServiceProvider()); + + $app['security.firewalls'] = array( + 'my-firewall' => array( + 'pattern' => '^/secure$', + 'form' => true, + 'logout' => true, + 'remember_me' => array( + 'key' => 'Choose_A_Unique_Random_Key', + 'always_remember_me' => true, + /* Other options */ + ), + 'users' => array( /* ... */ ), + ), + ); + +Options +------- + +* **key**: A secret key to generate tokens (you should generate a random + string). + +* **name**: Cookie name (default: ``REMEMBERME``). + +* **lifetime**: Cookie lifetime (default: ``31536000`` ~ 1 year). + +* **path**: Cookie path (default: ``/``). + +* **domain**: Cookie domain (default: ``null`` = request domain). + +* **secure**: Cookie is secure (default: ``false``). + +* **httponly**: Cookie is HTTP only (default: ``true``). + +* **always_remember_me**: Enable remember me (default: ``false``). + +* **remember_me_parameter**: Name of the request parameter enabling remember_me + on login. To add the checkbox to the login form. You can find more + information in the `Symfony cookbook + `_ + (default: ``_remember_me``). diff --git a/doc/providers/security.rst b/doc/providers/security.rst new file mode 100644 index 0000000..f84d318 --- /dev/null +++ b/doc/providers/security.rst @@ -0,0 +1,711 @@ +Security +======== + +The *SecurityServiceProvider* manages authentication and authorization for +your applications. + +Parameters +---------- + +* **security.hide_user_not_found** (optional): Defines whether to hide user not + found exception or not. Defaults to ``true``. + +* **security.encoder.bcrypt.cost** (optional): Defines BCrypt password encoder cost. Defaults to 13. + +Services +-------- + +* **security.token_storage**: Gives access to the user token. + +* **security.authorization_checker**: Allows to check authorizations for the + users. + +* **security.authentication_manager**: An instance of + `AuthenticationProviderManager + `_, + responsible for authentication. + +* **security.access_manager**: An instance of `AccessDecisionManager + `_, + responsible for authorization. + +* **security.session_strategy**: Define the session strategy used for + authentication (default to a migration strategy). + +* **security.user_checker**: Checks user flags after authentication. + +* **security.last_error**: Returns the last authentication errors when given a + Request object. + +* **security.encoder_factory**: Defines the encoding strategies for user + passwords (uses ``security.default_encoder``). + +* **security.default_encoder**: The encoder to use by default for all users (BCrypt). + +* **security.encoder.digest**: Digest password encoder. + +* **security.encoder.bcrypt**: BCrypt password encoder. + +* **security.encoder.pbkdf2**: Pbkdf2 password encoder. + +* **user**: Returns the current user + +.. note:: + + The service provider defines many other services that are used internally + but rarely need to be customized. + +Registering +----------- + +.. code-block:: php + + $app->register(new Silex\Provider\SecurityServiceProvider(), array( + 'security.firewalls' => // see below + )); + +.. note:: + + Add the Symfony Security Component as a dependency: + + .. code-block:: bash + + composer require symfony/security + +.. caution:: + + If you're using a form to authenticate users, you need to enable + ``SessionServiceProvider``. + +.. caution:: + + The security features are only available after the Application has been + booted. So, if you want to use it outside of the handling of a request, + don't forget to call ``boot()`` first:: + + $app->boot(); + +Usage +----- + +The Symfony Security component is powerful. To learn more about it, read the +`Symfony Security documentation +`_. + +.. tip:: + + When a security configuration does not behave as expected, enable logging + (with the Monolog extension for instance) as the Security Component logs a + lot of interesting information about what it does and why. + +Below is a list of recipes that cover some common use cases. + +Accessing the current User +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The current user information is stored in a token that is accessible via the +``security`` service:: + + $token = $app['security.token_storage']->getToken(); + +If there is no information about the user, the token is ``null``. If the user +is known, you can get it with a call to ``getUser()``:: + + if (null !== $token) { + $user = $token->getUser(); + } + +The user can be a string, an object with a ``__toString()`` method, or an +instance of `UserInterface +`_. + +Securing a Path with HTTP Authentication +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following configuration uses HTTP basic authentication to secure URLs +under ``/admin/``:: + + $app['security.firewalls'] = array( + 'admin' => array( + 'pattern' => '^/admin', + 'http' => true, + 'users' => array( + // raw password is foo + 'admin' => array('ROLE_ADMIN', '$2y$10$3i9/lVd8UOFIJ6PAMFt8gu3/r5g0qeCJvoSlLCsvMTythye19F77a'), + ), + ), + ); + +The ``pattern`` is a regular expression on the URL path; the ``http`` setting +tells the security layer to use HTTP basic authentication and the ``users`` +entry defines valid users. + +If you want to restrict the firewall by more than the URL pattern (like the +HTTP method, the client IP, the hostname, or any Request attributes), use an +instance of a `RequestMatcher +`_ +for the ``pattern`` option:: + + use Symfony/Component/HttpFoundation/RequestMatcher; + + $app['security.firewalls'] = array( + 'admin' => array( + 'pattern' => new RequestMatcher('^/admin', 'example.com', 'POST'), + // ... + ), + ); + +Each user is defined with the following information: + +* The role or an array of roles for the user (roles are strings beginning with + ``ROLE_`` and ending with anything you want); + +* The user encoded password. + +.. caution:: + + All users must at least have one role associated with them. + +The default configuration of the extension enforces encoded passwords. To +generate a valid encoded password from a raw password, use the +``security.encoder_factory`` service:: + + // find the encoder for a UserInterface instance + $encoder = $app['security.encoder_factory']->getEncoder($user); + + // compute the encoded password for foo + $password = $encoder->encodePassword('foo', $user->getSalt()); + +When the user is authenticated, the user stored in the token is an instance of +`User +`_ + +.. caution:: + + If you are using php-cgi under Apache, you need to add this configuration + to make things work correctly: + + .. code-block:: apache + + RewriteEngine On + RewriteCond %{HTTP:Authorization} ^(.+)$ + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^(.*)$ app.php [QSA,L] + +Securing a Path with a Form +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Using a form to authenticate users is very similar to the above configuration. +Instead of using the ``http`` setting, use the ``form`` one and define these +two parameters: + +* **login_path**: The login path where the user is redirected when they are + accessing a secured area without being authenticated so that they can enter + their credentials; + +* **check_path**: The check URL used by Symfony to validate the credentials of + the user. + +Here is how to secure all URLs under ``/admin/`` with a form:: + + $app['security.firewalls'] = array( + 'admin' => array( + 'pattern' => '^/admin/', + 'form' => array('login_path' => '/login', 'check_path' => '/admin/login_check'), + 'users' => array( + 'admin' => array('ROLE_ADMIN', '$2y$10$3i9/lVd8UOFIJ6PAMFt8gu3/r5g0qeCJvoSlLCsvMTythye19F77a'), + ), + ), + ); + +Always keep in mind the following two golden rules: + +* The ``login_path`` path must always be defined **outside** the secured area + (or if it is in the secured area, the ``anonymous`` authentication mechanism + must be enabled -- see below); + +* The ``check_path`` path must always be defined **inside** the secured area. + +For the login form to work, create a controller like the following:: + + use Symfony\Component\HttpFoundation\Request; + + $app->get('/login', function(Request $request) use ($app) { + return $app['twig']->render('login.html', array( + 'error' => $app['security.last_error']($request), + 'last_username' => $app['session']->get('_security.last_username'), + )); + }); + +The ``error`` and ``last_username`` variables contain the last authentication +error and the last username entered by the user in case of an authentication +error. + +Create the associated template: + +.. code-block:: jinja + +
+ {{ error }} + + + +
+ +.. note:: + + The ``admin_login_check`` route is automatically defined by Silex and its + name is derived from the ``check_path`` value (all ``/`` are replaced with + ``_`` and the leading ``/`` is stripped). + +Defining more than one Firewall +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You are not limited to define one firewall per project. + +Configuring several firewalls is useful when you want to secure different +parts of your website with different authentication strategies or for +different users (like using an HTTP basic authentication for the website API +and a form to secure your website administration area). + +It's also useful when you want to secure all URLs except the login form:: + + $app['security.firewalls'] = array( + 'login' => array( + 'pattern' => '^/login$', + ), + 'secured' => array( + 'pattern' => '^.*$', + 'form' => array('login_path' => '/login', 'check_path' => '/login_check'), + 'users' => array( + 'admin' => array('ROLE_ADMIN', '$2y$10$3i9/lVd8UOFIJ6PAMFt8gu3/r5g0qeCJvoSlLCsvMTythye19F77a'), + ), + ), + ); + +The order of the firewall configurations is significant as the first one to +match wins. The above configuration first ensures that the ``/login`` URL is +not secured (no authentication settings), and then it secures all other URLs. + +.. tip:: + + You can toggle all registered authentication mechanisms for a particular + area on and off with the ``security`` flag:: + + $app['security.firewalls'] = array( + 'api' => array( + 'pattern' => '^/api', + 'security' => $app['debug'] ? false : true, + 'wsse' => true, + + // ... + ), + ); + +Adding a Logout +~~~~~~~~~~~~~~~ + +When using a form for authentication, you can let users log out if you add the +``logout`` setting, where ``logout_path`` must match the main firewall +pattern:: + + $app['security.firewalls'] = array( + 'secured' => array( + 'pattern' => '^/admin/', + 'form' => array('login_path' => '/login', 'check_path' => '/admin/login_check'), + 'logout' => array('logout_path' => '/admin/logout', 'invalidate_session' => true), + + // ... + ), + ); + +A route is automatically generated, based on the configured path (all ``/`` +are replaced with ``_`` and the leading ``/`` is stripped): + +.. code-block:: jinja + + Logout + +Allowing Anonymous Users +~~~~~~~~~~~~~~~~~~~~~~~~ + +When securing only some parts of your website, the user information are not +available in non-secured areas. To make the user accessible in such areas, +enabled the ``anonymous`` authentication mechanism:: + + $app['security.firewalls'] = array( + 'unsecured' => array( + 'anonymous' => true, + + // ... + ), + ); + +When enabling the anonymous setting, a user will always be accessible from the +security context; if the user is not authenticated, it returns the ``anon.`` +string. + +Checking User Roles +~~~~~~~~~~~~~~~~~~~ + +To check if a user is granted some role, use the ``isGranted()`` method on the +security context:: + + if ($app['security.authorization_checker']->isGranted('ROLE_ADMIN')) { + // ... + } + +You can check roles in Twig templates too: + +.. code-block:: jinja + + {% if is_granted('ROLE_ADMIN') %} + Switch to Fabien + {% endif %} + +You can check if a user is "fully authenticated" (not an anonymous user for +instance) with the special ``IS_AUTHENTICATED_FULLY`` role: + +.. code-block:: jinja + + {% if is_granted('IS_AUTHENTICATED_FULLY') %} + Logout + {% else %} + Login + {% endif %} + +Of course you will need to define a ``login`` route for this to work. + +.. tip:: + + Don't use the ``getRoles()`` method to check user roles. + +.. caution:: + + ``isGranted()`` throws an exception when no authentication information is + available (which is the case on non-secured area). + +Impersonating a User +~~~~~~~~~~~~~~~~~~~~ + +If you want to be able to switch to another user (without knowing the user +credentials), enable the ``switch_user`` authentication strategy:: + + $app['security.firewalls'] = array( + 'unsecured' => array( + 'switch_user' => array('parameter' => '_switch_user', 'role' => 'ROLE_ALLOWED_TO_SWITCH'), + + // ... + ), + ); + +Switching to another user is now a matter of adding the ``_switch_user`` query +parameter to any URL when logged in as a user who has the +``ROLE_ALLOWED_TO_SWITCH`` role: + +.. code-block:: jinja + + {% if is_granted('ROLE_ALLOWED_TO_SWITCH') %} + Switch to user Fabien + {% endif %} + +You can check that you are impersonating a user by checking the special +``ROLE_PREVIOUS_ADMIN``. This is useful for instance to allow the user to +switch back to their primary account: + +.. code-block:: jinja + + {% if is_granted('ROLE_PREVIOUS_ADMIN') %} + You are an admin but you've switched to another user, + exit the switch. + {% endif %} + +Defining a Role Hierarchy +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Defining a role hierarchy allows to automatically grant users some additional +roles:: + + $app['security.role_hierarchy'] = array( + 'ROLE_ADMIN' => array('ROLE_USER', 'ROLE_ALLOWED_TO_SWITCH'), + ); + +With this configuration, all users with the ``ROLE_ADMIN`` role also +automatically have the ``ROLE_USER`` and ``ROLE_ALLOWED_TO_SWITCH`` roles. + +Defining Access Rules +~~~~~~~~~~~~~~~~~~~~~ + +Roles are a great way to adapt the behavior of your website depending on +groups of users, but they can also be used to further secure some areas by +defining access rules:: + + $app['security.access_rules'] = array( + array('^/admin', 'ROLE_ADMIN', 'https'), + array('^.*$', 'ROLE_USER'), + ); + +With the above configuration, users must have the ``ROLE_ADMIN`` to access the +``/admin`` section of the website, and ``ROLE_USER`` for everything else. +Furthermore, the admin section can only be accessible via HTTPS (if that's not +the case, the user will be automatically redirected). + +.. note:: + + The first argument can also be a `RequestMatcher + `_ + instance. + +Defining a custom User Provider +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Using an array of users is simple and useful when securing an admin section of +a personal website, but you can override this default mechanism with you own. + +The ``users`` setting can be defined as a service that returns an instance of +`UserProviderInterface +`_:: + + 'users' => function () use ($app) { + return new UserProvider($app['db']); + }, + +Here is a simple example of a user provider, where Doctrine DBAL is used to +store the users:: + + use Symfony\Component\Security\Core\User\UserProviderInterface; + use Symfony\Component\Security\Core\User\UserInterface; + use Symfony\Component\Security\Core\User\User; + use Symfony\Component\Security\Core\Exception\UnsupportedUserException; + use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; + use Doctrine\DBAL\Connection; + + class UserProvider implements UserProviderInterface + { + private $conn; + + public function __construct(Connection $conn) + { + $this->conn = $conn; + } + + public function loadUserByUsername($username) + { + $stmt = $this->conn->executeQuery('SELECT * FROM users WHERE username = ?', array(strtolower($username))); + + if (!$user = $stmt->fetch()) { + throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username)); + } + + return new User($user['username'], $user['password'], explode(',', $user['roles']), true, true, true, true); + } + + public function refreshUser(UserInterface $user) + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user))); + } + + return $this->loadUserByUsername($user->getUsername()); + } + + public function supportsClass($class) + { + return $class === 'Symfony\Component\Security\Core\User\User'; + } + } + +In this example, instances of the default ``User`` class are created for the +users, but you can define your own class; the only requirement is that the +class must implement `UserInterface +`_ + +And here is the code that you can use to create the database schema and some +sample users:: + + use Doctrine\DBAL\Schema\Table; + + $schema = $app['db']->getSchemaManager(); + if (!$schema->tablesExist('users')) { + $users = new Table('users'); + $users->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => true)); + $users->setPrimaryKey(array('id')); + $users->addColumn('username', 'string', array('length' => 32)); + $users->addUniqueIndex(array('username')); + $users->addColumn('password', 'string', array('length' => 255)); + $users->addColumn('roles', 'string', array('length' => 255)); + + $schema->createTable($users); + + $app['db']->insert('users', array( + 'username' => 'fabien', + 'password' => '$2y$10$3i9/lVd8UOFIJ6PAMFt8gu3/r5g0qeCJvoSlLCsvMTythye19F77a', + 'roles' => 'ROLE_USER' + )); + + $app['db']->insert('users', array( + 'username' => 'admin', + 'password' => '$2y$10$3i9/lVd8UOFIJ6PAMFt8gu3/r5g0qeCJvoSlLCsvMTythye19F77a', + 'roles' => 'ROLE_ADMIN' + )); + } + +.. tip:: + + If you are using the Doctrine ORM, the Symfony bridge for Doctrine + provides a user provider class that is able to load users from your + entities. + +Defining a custom Encoder +~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, Silex uses the ``BCrypt`` algorithm to encode passwords. +Additionally, the password is encoded multiple times. +You can change these defaults by overriding ``security.default_encoder`` +service to return one of the predefined encoders: + +* **security.encoder.digest**: Digest password encoder. + +* **security.encoder.bcrypt**: BCrypt password encoder. + +* **security.encoder.pbkdf2**: Pbkdf2 password encoder. + +.. code-block:: php + + $app['security.default_encoder'] = function ($app) { + return $app['security.encoder.pbkdf2']; + }; + +Or you can define you own, fully customizable encoder:: + + use Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder; + + $app['security.default_encoder'] = function ($app) { + // Plain text (e.g. for debugging) + return new PlaintextPasswordEncoder(); + }; + +.. tip:: + + You can change the default BCrypt encoding cost by overriding ``security.encoder.bcrypt.cost`` + +Defining a custom Authentication Provider +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Symfony Security component provides a lot of ready-to-use authentication +providers (form, HTTP, X509, remember me, ...), but you can add new ones +easily. To register a new authentication provider, create a service named +``security.authentication_listener.factory.XXX`` where ``XXX`` is the name you want to +use in your configuration:: + + $app['security.authentication_listener.factory.wsse'] = $app->protect(function ($name, $options) use ($app) { + // define the authentication provider object + $app['security.authentication_provider.'.$name.'.wsse'] = function () use ($app) { + return new WsseProvider($app['security.user_provider.default'], __DIR__.'/security_cache'); + }; + + // define the authentication listener object + $app['security.authentication_listener.'.$name.'.wsse'] = function () use ($app) { + return new WsseListener($app['security.token_storage'], $app['security.authentication_manager']); + }; + + return array( + // the authentication provider id + 'security.authentication_provider.'.$name.'.wsse', + // the authentication listener id + 'security.authentication_listener.'.$name.'.wsse', + // the entry point id + null, + // the position of the listener in the stack + 'pre_auth' + ); + }); + +You can now use it in your configuration like any other built-in +authentication provider:: + + $app->register(new Silex\Provider\SecurityServiceProvider(), array( + 'security.firewalls' => array( + 'default' => array( + 'wsse' => true, + + // ... + ), + ), + )); + +Instead of ``true``, you can also define an array of options that customize +the behavior of your authentication factory; it will be passed as the second +argument of your authentication factory (see above). + +This example uses the authentication provider classes as described in the +Symfony `cookbook`_. + + +.. note:: + + The Guard component simplifies the creation of custom authentication + providers. :doc:`How to Create a Custom Authentication System with Guard + ` + +Stateless Authentication +~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, a session cookie is created to persist the security context of +the user. However, if you use certificates, HTTP authentication, WSSE and so +on, the credentials are sent for each request. In that case, you can turn off +persistence by activating the ``stateless`` authentication flag:: + + $app['security.firewalls'] = array( + 'default' => array( + 'stateless' => true, + 'wsse' => true, + + // ... + ), + ); + +Traits +------ + +``Silex\Application\SecurityTrait`` adds the following shortcuts: + +* **encodePassword**: Encode a given password. + +.. code-block:: php + + $user = $app->user(); + + $encoded = $app->encodePassword($user, 'foo'); + +``Silex\Route\SecurityTrait`` adds the following methods to the controllers: + +* **secure**: Secures a controller for the given roles. + +.. code-block:: php + + $app->get('/', function () { + // do something but only for admins + })->secure('ROLE_ADMIN'); + +.. caution:: + + The ``Silex\Route\SecurityTrait`` must be used with a user defined + ``Route`` class, not the application. + + .. code-block:: php + + use Silex\Route; + + class MyRoute extends Route + { + use Route\SecurityTrait; + } + + .. code-block:: php + + $app['route_class'] = 'MyRoute'; + + +.. _cookbook: http://symfony.com/doc/current/cookbook/security/custom_authentication_provider.html diff --git a/doc/providers/serializer.rst b/doc/providers/serializer.rst new file mode 100644 index 0000000..162dbab --- /dev/null +++ b/doc/providers/serializer.rst @@ -0,0 +1,73 @@ +Serializer +========== + +The *SerializerServiceProvider* provides a service for serializing objects. + +Parameters +---------- + +None. + +Services +-------- + +* **serializer**: An instance of `Symfony\\Component\\Serializer\\Serializer + `_. + +* **serializer.encoders**: `Symfony\\Component\\Serializer\\Encoder\\JsonEncoder + `_ + and `Symfony\\Component\\Serializer\\Encoder\\XmlEncoder + `_. + +* **serializer.normalizers**: `Symfony\\Component\\Serializer\\Normalizer\\CustomNormalizer + `_ + and `Symfony\\Component\\Serializer\\Normalizer\\GetSetMethodNormalizer + `_. + +Registering +----------- + +.. code-block:: php + + $app->register(new Silex\Provider\SerializerServiceProvider()); + +.. note:: + + Add the Symfony's `Serializer Component + `_ as a + dependency: + + .. code-block:: bash + + composer require symfony/serializer + +Usage +----- + +The ``SerializerServiceProvider`` provider provides a ``serializer`` service:: + + use Silex\Application; + use Silex\Provider\SerializerServiceProvider; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + $app = new Application(); + + $app->register(new SerializerServiceProvider()); + + // only accept content types supported by the serializer via the assert method. + $app->get("/pages/{id}.{_format}", function (Request $request, $id) use ($app) { + // assume a page_repository service exists that returns Page objects. The + // object returned has getters and setters exposing the state. + $page = $app['page_repository']->find($id); + $format = $request->getRequestFormat(); + + if (!$page instanceof Page) { + $app->abort("No page found for id: $id"); + } + + return new Response($app['serializer']->serialize($page, $format), 200, array( + "Content-Type" => $request->getMimeType($format) + )); + })->assert("_format", "xml|json") + ->assert("id", "\d+"); diff --git a/doc/providers/service_controller.rst b/doc/providers/service_controller.rst new file mode 100644 index 0000000..15bca28 --- /dev/null +++ b/doc/providers/service_controller.rst @@ -0,0 +1,142 @@ +Service Controllers +=================== + +As your Silex application grows, you may wish to begin organizing your +controllers in a more formal fashion. Silex can use controller classes out of +the box, but with a bit of work, your controllers can be created as services, +giving you the full power of dependency injection and lazy loading. + +.. ::todo Link above to controller classes cookbook + +Why would I want to do this? +---------------------------- + +- Dependency Injection over Service Location + + Using this method, you can inject the actual dependencies required by your + controller and gain total inversion of control, while still maintaining the + lazy loading of your controllers and its dependencies. Because your + dependencies are clearly defined, they are easily mocked, allowing you to test + your controllers in isolation. + +- Framework Independence + + Using this method, your controllers start to become more independent of the + framework you are using. Carefully crafted, your controllers will become + reusable with multiple frameworks. By keeping careful control of your + dependencies, your controllers could easily become compatible with Silex, + Symfony (full stack) and Drupal, to name just a few. + +Parameters +---------- + +There are currently no parameters for the ``ServiceControllerServiceProvider``. + +Services +-------- + +There are no extra services provided, the ``ServiceControllerServiceProvider`` +simply extends the existing **resolver** service. + +Registering +----------- + +.. code-block:: php + + $app->register(new Silex\Provider\ServiceControllerServiceProvider()); + +Usage +----- + +In this slightly contrived example of a blog API, we're going to change the +``/posts.json`` route to use a controller, that is defined as a service. + +.. code-block:: php + + use Silex\Application; + use Demo\Repository\PostRepository; + + $app = new Application(); + + $app['posts.repository'] = function() { + return new PostRepository; + }; + + $app->get('/posts.json', function() use ($app) { + return $app->json($app['posts.repository']->findAll()); + }); + +Rewriting your controller as a service is pretty simple, create a Plain Ol' PHP +Object with your ``PostRepository`` as a dependency, along with an +``indexJsonAction`` method to handle the request. Although not shown in the +example below, you can use type hinting and parameter naming to get the +parameters you need, just like with standard Silex routes. + +If you are a TDD/BDD fan (and you should be), you may notice that this +controller has well defined responsibilities and dependencies, and is easily +tested/specced. You may also notice that the only external dependency is on +``Symfony\Component\HttpFoundation\JsonResponse``, meaning this controller could +easily be used in a Symfony (full stack) application, or potentially with other +applications or frameworks that know how to handle a `Symfony/HttpFoundation +`_ +``Response`` object. + +.. code-block:: php + + namespace Demo\Controller; + + use Demo\Repository\PostRepository; + use Symfony\Component\HttpFoundation\JsonResponse; + + class PostController + { + protected $repo; + + public function __construct(PostRepository $repo) + { + $this->repo = $repo; + } + + public function indexJsonAction() + { + return new JsonResponse($this->repo->findAll()); + } + } + +And lastly, define your controller as a service in the application, along with +your route. The syntax in the route definition is the name of the service, +followed by a single colon (:), followed by the method name. + +.. code-block:: php + + $app['posts.controller'] = function() use ($app) { + return new PostController($app['posts.repository']); + }; + + $app->get('/posts.json', "posts.controller:indexJsonAction"); + +In addition to using classes for service controllers, you can define any +callable as a service in the application to be used for a route. + +.. code-block:: php + + namespace Demo\Controller; + + use Demo\Repository\PostRepository; + use Symfony\Component\HttpFoundation\JsonResponse; + + function postIndexJson(PostRepository $repo) { + return function() use ($repo) { + return new JsonResponse($repo->findAll()); + }; + } + +And when defining your route, the code would look like the following: + +.. code-block:: php + + $app['posts.controller'] = function($app) { + return Demo\Controller\postIndexJson($app['posts.repository']); + }; + + $app->get('/posts.json', 'posts.controller'); diff --git a/doc/providers/session.rst b/doc/providers/session.rst new file mode 100644 index 0000000..301385d --- /dev/null +++ b/doc/providers/session.rst @@ -0,0 +1,103 @@ +Session +======= + +The *SessionServiceProvider* provides a service for storing data persistently +between requests. + +Parameters +---------- + +* **session.storage.save_path** (optional): The path for the + ``NativeFileSessionHandler``, defaults to the value of + ``sys_get_temp_dir()``. + +* **session.storage.options**: An array of options that is passed to the + constructor of the ``session.storage`` service. + + In case of the default `NativeSessionStorage + `_, + the most useful options are: + + * **name**: The cookie name (_SESS by default) + * **id**: The session id (null by default) + * **cookie_lifetime**: Cookie lifetime + * **cookie_path**: Cookie path + * **cookie_domain**: Cookie domain + * **cookie_secure**: Cookie secure (HTTPS) + * **cookie_httponly**: Whether the cookie is http only + + However, all of these are optional. Default Sessions life time is 1800 + seconds (30 minutes). To override this, set the ``lifetime`` option. + + For a full list of available options, read the `PHP + `_ official documentation. + +* **session.test**: Whether to simulate sessions or not (useful when writing + functional tests). + +Services +-------- + +* **session**: An instance of Symfony's `Session + `_. + +* **session.storage**: A service that is used for persistence of the session + data. + +* **session.storage.handler**: A service that is used by the + ``session.storage`` for data access. Defaults to a `NativeFileSessionHandler + `_ + storage handler. + +Registering +----------- + +.. code-block:: php + + $app->register(new Silex\Provider\SessionServiceProvider()); + +Usage +----- + +The Session provider provides a ``session`` service. Here is an example that +authenticates a user and creates a session for them:: + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + $app->get('/login', function (Request $request) use ($app) { + $username = $request->server->get('PHP_AUTH_USER', false); + $password = $request->server->get('PHP_AUTH_PW'); + + if ('igor' === $username && 'password' === $password) { + $app['session']->set('user', array('username' => $username)); + return $app->redirect('/account'); + } + + $response = new Response(); + $response->headers->set('WWW-Authenticate', sprintf('Basic realm="%s"', 'site_login')); + $response->setStatusCode(401, 'Please sign in.'); + return $response; + }); + + $app->get('/account', function () use ($app) { + if (null === $user = $app['session']->get('user')) { + return $app->redirect('/login'); + } + + return "Welcome {$user['username']}!"; + }); + + +Custom Session Configurations +----------------------------- + +If your system is using a custom session configuration (such as a redis handler +from a PHP extension) then you need to disable the NativeFileSessionHandler by +setting ``session.storage.handler`` to null. You will have to configure the +``session.save_path`` ini setting yourself in that case. + +.. code-block:: php + + $app['session.storage.handler'] = null; + diff --git a/doc/providers/swiftmailer.rst b/doc/providers/swiftmailer.rst new file mode 100644 index 0000000..f3153f1 --- /dev/null +++ b/doc/providers/swiftmailer.rst @@ -0,0 +1,146 @@ +Swiftmailer +=========== + +The *SwiftmailerServiceProvider* provides a service for sending email through +the `Swift Mailer `_ library. + +You can use the ``mailer`` service to send messages easily. By default, it +will attempt to send emails through SMTP. + +Parameters +---------- + +* **swiftmailer.use_spool**: A boolean to specify whether or not to use the + memory spool, defaults to true. + +* **swiftmailer.options**: An array of options for the default SMTP-based + configuration. + + The following options can be set: + + * **host**: SMTP hostname, defaults to 'localhost'. + * **port**: SMTP port, defaults to 25. + * **username**: SMTP username, defaults to an empty string. + * **password**: SMTP password, defaults to an empty string. + * **encryption**: SMTP encryption, defaults to null. Valid values are 'tls', 'ssl', or null (indicating no encryption). + * **auth_mode**: SMTP authentication mode, defaults to null. Valid values are 'plain', 'login', 'cram-md5', or null. + + Example usage:: + + $app['swiftmailer.options'] = array( + 'host' => 'host', + 'port' => '25', + 'username' => 'username', + 'password' => 'password', + 'encryption' => null, + 'auth_mode' => null + ); + +* **swiftmailer.sender_address**: If set, all messages will be delivered with + this address as the "return path" address. + +* **swiftmailer.delivery_addresses**: If not empty, all email messages will be + sent to those addresses instead of being sent to their actual recipients. This + is often useful when developing. + +* **swiftmailer.delivery_whitelist**: Used in combination with + ``delivery_addresses``. If set, emails matching any of these patterns will be + delivered like normal, as well as being sent to ``delivery_addresses``. + +Services +-------- + +* **mailer**: The mailer instance. + + Example usage:: + + $message = \Swift_Message::newInstance(); + + // ... + + $app['mailer']->send($message); + +* **swiftmailer.transport**: The transport used for e-mail + delivery. Defaults to a ``Swift_Transport_EsmtpTransport``. + +* **swiftmailer.transport.buffer**: StreamBuffer used by + the transport. + +* **swiftmailer.transport.authhandler**: Authentication + handler used by the transport. Will try the following + by default: CRAM-MD5, login, plaintext. + +* **swiftmailer.transport.eventdispatcher**: Internal event + dispatcher used by Swiftmailer. + +Registering +----------- + +.. code-block:: php + + $app->register(new Silex\Provider\SwiftmailerServiceProvider()); + +.. note:: + + Add SwiftMailer as a dependency: + + .. code-block:: bash + + composer require swiftmailer/swiftmailer + +Usage +----- + +The Swiftmailer provider provides a ``mailer`` service:: + + use Symfony\Component\HttpFoundation\Request; + + $app->post('/feedback', function (Request $request) use ($app) { + $message = \Swift_Message::newInstance() + ->setSubject('[YourSite] Feedback') + ->setFrom(array('noreply@yoursite.com')) + ->setTo(array('feedback@yoursite.com')) + ->setBody($request->get('message')); + + $app['mailer']->send($message); + + return new Response('Thank you for your feedback!', 201); + }); + +Usage in commands +~~~~~~~~~~~~~~~~~ + +By default, the Swiftmailer provider sends the emails using the ``KernelEvents::TERMINATE`` +event, which is fired after the response has been sent. However, as this event +isn't fired for console commands, your emails won't be sent. + +For that reason, if you send emails using a command console, it is recommended +that you disable the use of the memory spool (before accessing ``$app['mailer']``):: + + $app['swiftmailer.use_spool'] = false; + +Alternatively, you can just make sure to flush the message spool by hand before +ending the command execution. To do so, use the following code:: + + $app['swiftmailer.spooltransport'] + ->getSpool() + ->flushQueue($app['swiftmailer.transport']) + ; + +Traits +------ + +``Silex\Application\SwiftmailerTrait`` adds the following shortcuts: + +* **mail**: Sends an email. + +.. code-block:: php + + $app->mail(\Swift_Message::newInstance() + ->setSubject('[YourSite] Feedback') + ->setFrom(array('noreply@yoursite.com')) + ->setTo(array('feedback@yoursite.com')) + ->setBody($request->get('message'))); + +For more information, check out the `Swift Mailer documentation +`_. diff --git a/doc/providers/translation.rst b/doc/providers/translation.rst new file mode 100644 index 0000000..145fc18 --- /dev/null +++ b/doc/providers/translation.rst @@ -0,0 +1,193 @@ +Translation +=========== + +The *TranslationServiceProvider* provides a service for translating your +application into different languages. + +Parameters +---------- + +* **translator.domains** (optional): A mapping of domains/locales/messages. + This parameter contains the translation data for all languages and domains. + +* **locale** (optional): The locale for the translator. You will most likely + want to set this based on some request parameter. Defaults to ``en``. + +* **locale_fallbacks** (optional): Fallback locales for the translator. It will + be used when the current locale has no messages set. Defaults to ``en``. + +Services +-------- + +* **translator**: An instance of `Translator + `_, + that is used for translation. + +* **translator.loader**: An instance of an implementation of the translation + `LoaderInterface + `_, + defaults to an `ArrayLoader + `_. + +* **translator.message_selector**: An instance of `MessageSelector + `_. + +Registering +----------- + +.. code-block:: php + + $app->register(new Silex\Provider\LocaleServiceProvider()); + $app->register(new Silex\Provider\TranslationServiceProvider(), array( + 'locale_fallbacks' => array('en'), + )); + +.. note:: + + Add the Symfony Translation Component as a dependency: + + .. code-block:: bash + + composer require symfony/translation + +Usage +----- + +The Translation provider provides a ``translator`` service and makes use of +the ``translator.domains`` parameter:: + + $app['translator.domains'] = array( + 'messages' => array( + 'en' => array( + 'hello' => 'Hello %name%', + 'goodbye' => 'Goodbye %name%', + ), + 'de' => array( + 'hello' => 'Hallo %name%', + 'goodbye' => 'Tschüss %name%', + ), + 'fr' => array( + 'hello' => 'Bonjour %name%', + 'goodbye' => 'Au revoir %name%', + ), + ), + 'validators' => array( + 'fr' => array( + 'This value should be a valid number.' => 'Cette valeur doit être un nombre.', + ), + ), + ); + + $app->get('/{_locale}/{message}/{name}', function ($message, $name) use ($app) { + return $app['translator']->trans($message, array('%name%' => $name)); + }); + +The above example will result in following routes: + +* ``/en/hello/igor`` will return ``Hello igor``. + +* ``/de/hello/igor`` will return ``Hallo igor``. + +* ``/fr/hello/igor`` will return ``Bonjour igor``. + +* ``/it/hello/igor`` will return ``Hello igor`` (because of the fallback). + +Using Resources +--------------- + +When translations are stored in a file, you can load them as follows:: + + $app = new Application(); + + $app->register(new TranslationServiceProvider()); + $app->extend('translator.resources', function ($resources, $app) { + $resources = array_merge($resources, array( + array('array', array('This value should be a valid number.' => 'Cette valeur doit être un nombre.'), 'fr', 'validators'), + )); + + return $resources; + }); + +Traits +------ + +``Silex\Application\TranslationTrait`` adds the following shortcuts: + +* **trans**: Translates the given message. + +* **transChoice**: Translates the given choice message by choosing a + translation according to a number. + +.. code-block:: php + + $app->trans('Hello World'); + + $app->transChoice('Hello World'); + +Recipes +------- + +YAML-based language files +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Having your translations in PHP files can be inconvenient. This recipe will +show you how to load translations from external YAML files. + +First, add the Symfony ``Config`` and ``Yaml`` components as dependencies: + +.. code-block:: bash + + composer require symfony/config symfony/yaml + +Next, you have to create the language mappings in YAML files. A naming you can +use is ``locales/en.yml``. Just do the mapping in this file as follows: + +.. code-block:: yaml + + hello: Hello %name% + goodbye: Goodbye %name% + +Then, register the ``YamlFileLoader`` on the ``translator`` and add all your +translation files:: + + use Symfony\Component\Translation\Loader\YamlFileLoader; + + $app->extend('translator', function($translator, $app) { + $translator->addLoader('yaml', new YamlFileLoader()); + + $translator->addResource('yaml', __DIR__.'/locales/en.yml', 'en'); + $translator->addResource('yaml', __DIR__.'/locales/de.yml', 'de'); + $translator->addResource('yaml', __DIR__.'/locales/fr.yml', 'fr'); + + return $translator; + }); + +XLIFF-based language files +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Just as you would do with YAML translation files, you first need to add the +Symfony ``Config`` component as a dependency (see above for details). + +Then, similarly, create XLIFF files in your locales directory and add them to +the translator:: + + $translator->addResource('xliff', __DIR__.'/locales/en.xlf', 'en'); + $translator->addResource('xliff', __DIR__.'/locales/de.xlf', 'de'); + $translator->addResource('xliff', __DIR__.'/locales/fr.xlf', 'fr'); + +.. note:: + + The XLIFF loader is already pre-configured by the extension. + +Accessing translations in Twig templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once loaded, the translation service provider is available from within Twig +templates when using the Twig bridge provided by Symfony (see +:doc:`TwigServiceProvider
`): + +.. code-block:: jinja + + {{ 'translation_key'|trans }} + {{ 'translation_key'|transchoice }} + {% trans %}translation_key{% endtrans %} diff --git a/doc/providers/twig.rst b/doc/providers/twig.rst new file mode 100644 index 0000000..dd8a85b --- /dev/null +++ b/doc/providers/twig.rst @@ -0,0 +1,200 @@ +Twig +==== + +The *TwigServiceProvider* provides integration with the `Twig +`_ template engine. + +Parameters +---------- + +* **twig.path** (optional): Path to the directory containing twig template + files (it can also be an array of paths). + +* **twig.templates** (optional): An associative array of template names to + template contents. Use this if you want to define your templates inline. + +* **twig.options** (optional): An associative array of twig + options. Check out the `twig documentation `_ + for more information. + +* **twig.form.templates** (optional): An array of templates used to render + forms (only available when the ``FormServiceProvider`` is enabled). The + default theme is ``form_div_layout.html.twig``, but you can use the other + built-in themes: ``form_table_layout.html.twig``, + ``bootstrap_3_layout.html.twig``, and + ``bootstrap_3_horizontal_layout.html.twig``. + +Services +-------- + +* **twig**: The ``Twig_Environment`` instance. The main way of + interacting with Twig. + +* **twig.loader**: The loader for Twig templates which uses the ``twig.path`` + and the ``twig.templates`` options. You can also replace the loader + completely. + +Registering +----------- + +.. code-block:: php + + $app->register(new Silex\Provider\TwigServiceProvider(), array( + 'twig.path' => __DIR__.'/views', + )); + +.. note:: + + Add Twig as a dependency: + + .. code-block:: bash + + composer require twig/twig + +Usage +----- + +The Twig provider provides a ``twig`` service that can render templates:: + + $app->get('/hello/{name}', function ($name) use ($app) { + return $app['twig']->render('hello.twig', array( + 'name' => $name, + )); + }); + +Symfony Components Integration +------------------------------ + +Symfony provides a Twig bridge that provides additional integration between +some Symfony components and Twig. Add it as a dependency: + +.. code-block:: bash + + composer require symfony/twig-bridge + +When present, the ``TwigServiceProvider`` will provide you with the following +additional capabilities. + +* Access to the ``path()`` and ``url()`` functions. You can find more + information in the `Symfony Routing documentation + `_: + + .. code-block:: jinja + + {{ path('homepage') }} + {{ url('homepage') }} {# generates the absolute url http://example.org/ #} + {{ path('hello', {name: 'Fabien'}) }} + {{ url('hello', {name: 'Fabien'}) }} {# generates the absolute url http://example.org/hello/Fabien #} + +* Access to the ``absolute_url()`` and ``relative_path()`` Twig functions. + +Translations Support +~~~~~~~~~~~~~~~~~~~~ + +If you are using the ``TranslationServiceProvider``, you will get the +``trans()`` and ``transchoice()`` functions for translation in Twig templates. +You can find more information in the `Symfony Translation documentation +`_. + +Form Support +~~~~~~~~~~~~ + +If you are using the ``FormServiceProvider``, you will get a set of helpers for +working with forms in templates. You can find more information in the `Symfony +Forms reference +`_. + +Security Support +~~~~~~~~~~~~~~~~ + +If you are using the ``SecurityServiceProvider``, you will have access to the +``is_granted()`` function in templates. You can find more information in the +`Symfony Security documentation +`_. + +Global Variable +~~~~~~~~~~~~~~~ + +When the Twig bridge is available, the ``global`` variable refers to an +instance of `AppVariable `_. +It gives access to the following methods: + +.. code-block:: jinja + + {# The current Request #} + {{ global.request }} + + {# The current User (when security is enabled) #} + {{ global.user }} + + {# The current Session #} + {{ global.session }} + + {# The debug flag #} + {{ global.debug }} + +Rendering a Controller +~~~~~~~~~~~~~~~~~~~~~~ + +A ``render`` function is also registered to help you render another controller +from a template (available when the :doc:`HttpFragment Service Provider +
` is registered): + +.. code-block:: jinja + + {{ render(url('sidebar')) }} + + {# or you can reference a controller directly without defining a route for it #} + {{ render(controller(controller)) }} + +.. note:: + + You must prepend the ``app.request.baseUrl`` to render calls to ensure + that the render works when deployed into a sub-directory of the docroot. + +.. note:: + + Read the Twig `reference`_ for Symfony document to learn more about the + various Twig functions. + +Traits +------ + +``Silex\Application\TwigTrait`` adds the following shortcuts: + +* **render**: Renders a view with the given parameters and returns a Response + object. + +.. code-block:: php + + return $app->render('index.html', ['name' => 'Fabien']); + + $response = new Response(); + $response->setTtl(10); + + return $app->render('index.html', ['name' => 'Fabien'], $response); + +.. code-block:: php + + // stream a view + use Symfony\Component\HttpFoundation\StreamedResponse; + + return $app->render('index.html', ['name' => 'Fabien'], new StreamedResponse()); + +Customization +------------- + +You can configure the Twig environment before using it by extending the +``twig`` service:: + + $app->extend('twig', function($twig, $app) { + $twig->addGlobal('pi', 3.14); + $twig->addFilter('levenshtein', new \Twig_Filter_Function('levenshtein')); + + return $twig; + }); + +For more information, check out the `official Twig documentation +`_. + +.. _reference: https://symfony.com/doc/current/reference/twig_reference.html#controller diff --git a/doc/providers/validator.rst b/doc/providers/validator.rst new file mode 100644 index 0000000..bd4e998 --- /dev/null +++ b/doc/providers/validator.rst @@ -0,0 +1,217 @@ +Validator +========= + +The *ValidatorServiceProvider* provides a service for validating data. It is +most useful when used with the *FormServiceProvider*, but can also be used +standalone. + +Parameters +---------- + +* **validator.validator_service_ids**: An array of service names representing + validators. + +Services +-------- + +* **validator**: An instance of `Validator + `_. + +* **validator.mapping.class_metadata_factory**: Factory for metadata loaders, + which can read validation constraint information from classes. Defaults to + StaticMethodLoader--ClassMetadataFactory. + + This means you can define a static ``loadValidatorMetadata`` method on your + data class, which takes a ClassMetadata argument. Then you can set + constraints on this ClassMetadata instance. + +Registering +----------- + +.. code-block:: php + + $app->register(new Silex\Provider\ValidatorServiceProvider()); + +.. note:: + + Add the Symfony Validator Component as a dependency: + + .. code-block:: bash + + composer require symfony/validator + +Usage +----- + +The Validator provider provides a ``validator`` service. + +Validating Values +~~~~~~~~~~~~~~~~~ + +You can validate values directly using the ``validate`` validator +method:: + + use Symfony\Component\Validator\Constraints as Assert; + + $app->get('/validate/{email}', function ($email) use ($app) { + $errors = $app['validator']->validate($email, new Assert\Email()); + + if (count($errors) > 0) { + return (string) $errors; + } else { + return 'The email is valid'; + } + }); + +Validating Associative Arrays +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Validating associative arrays is like validating simple values, with a +collection of constraints:: + + use Symfony\Component\Validator\Constraints as Assert; + + $book = array( + 'title' => 'My Book', + 'author' => array( + 'first_name' => 'Fabien', + 'last_name' => 'Potencier', + ), + ); + + $constraint = new Assert\Collection(array( + 'title' => new Assert\Length(array('min' => 10)), + 'author' => new Assert\Collection(array( + 'first_name' => array(new Assert\NotBlank(), new Assert\Length(array('min' => 10))), + 'last_name' => new Assert\Length(array('min' => 10)), + )), + )); + $errors = $app['validator']->validate($book, $constraint); + + if (count($errors) > 0) { + foreach ($errors as $error) { + echo $error->getPropertyPath().' '.$error->getMessage()."\n"; + } + } else { + echo 'The book is valid'; + } + +Validating Objects +~~~~~~~~~~~~~~~~~~ + +If you want to add validations to a class, you can define the constraint for +the class properties and getters, and then call the ``validate`` method:: + + use Symfony\Component\Validator\Constraints as Assert; + + class Book + { + public $title; + public $author; + } + + class Author + { + public $first_name; + public $last_name; + } + + $author = new Author(); + $author->first_name = 'Fabien'; + $author->last_name = 'Potencier'; + + $book = new Book(); + $book->title = 'My Book'; + $book->author = $author; + + $metadata = $app['validator.mapping.class_metadata_factory']->getMetadataFor('Author'); + $metadata->addPropertyConstraint('first_name', new Assert\NotBlank()); + $metadata->addPropertyConstraint('first_name', new Assert\Length(array('min' => 10))); + $metadata->addPropertyConstraint('last_name', new Assert\Length(array('min' => 10))); + + $metadata = $app['validator.mapping.class_metadata_factory']->getMetadataFor('Book'); + $metadata->addPropertyConstraint('title', new Assert\Length(array('min' => 10))); + $metadata->addPropertyConstraint('author', new Assert\Valid()); + + $errors = $app['validator']->validate($book); + + if (count($errors) > 0) { + foreach ($errors as $error) { + echo $error->getPropertyPath().' '.$error->getMessage()."\n"; + } + } else { + echo 'The author is valid'; + } + +You can also declare the class constraint by adding a static +``loadValidatorMetadata`` method to your classes:: + + use Symfony\Component\Validator\Mapping\ClassMetadata; + use Symfony\Component\Validator\Constraints as Assert; + + class Book + { + public $title; + public $author; + + static public function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addPropertyConstraint('title', new Assert\Length(array('min' => 10))); + $metadata->addPropertyConstraint('author', new Assert\Valid()); + } + } + + class Author + { + public $first_name; + public $last_name; + + static public function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addPropertyConstraint('first_name', new Assert\NotBlank()); + $metadata->addPropertyConstraint('first_name', new Assert\Length(array('min' => 10))); + $metadata->addPropertyConstraint('last_name', new Assert\Length(array('min' => 10))); + } + } + + $app->get('/validate/{email}', function ($email) use ($app) { + $author = new Author(); + $author->first_name = 'Fabien'; + $author->last_name = 'Potencier'; + + $book = new Book(); + $book->title = 'My Book'; + $book->author = $author; + + $errors = $app['validator']->validate($book); + + if (count($errors) > 0) { + foreach ($errors as $error) { + echo $error->getPropertyPath().' '.$error->getMessage()."\n"; + } + } else { + echo 'The author is valid'; + } + }); + +.. note:: + + Use ``addGetterConstraint()`` to add constraints on getter methods and + ``addConstraint()`` to add constraints on the class itself. + +Translation +~~~~~~~~~~~ + +To be able to translate the error messages, you can use the translator +provider and register the messages under the ``validators`` domain:: + + $app['translator.domains'] = array( + 'validators' => array( + 'fr' => array( + 'This value should be a valid number.' => 'Cette valeur doit être un nombre.', + ), + ), + ); + +For more information, consult the `Symfony Validation documentation +`_. diff --git a/doc/providers/var_dumper.rst b/doc/providers/var_dumper.rst new file mode 100644 index 0000000..ea4dd19 --- /dev/null +++ b/doc/providers/var_dumper.rst @@ -0,0 +1,44 @@ +Var Dumper +========== + +The *VarDumperServiceProvider* provides a mechanism that allows exploring then +dumping any PHP variable. + +Parameters +---------- + +* **var_dumper.dump_destination**: A stream URL where dumps should be written + to (defaults to ``null``). + +Services +-------- + +* n/a + +Registering +----------- + +.. code-block:: php + + $app->register(new Silex\Provider\VarDumperServiceProvider()); + +.. note:: + + Add the Symfony VarDumper Component as a dependency: + + .. code-block:: bash + + composer require symfony/var-dumper + +Usage +----- + +Adding the VarDumper component as a Composer dependency gives you access to the +``dump()`` PHP function anywhere in your code. + +If you are using Twig, it also provides a ``dump()`` Twig function and a +``dump`` Twig tag. + +The VarDumperServiceProvider is also useful when used with the Silex +WebProfiler as the dumps are made available in the web debug toolbar and in the +web profiler. diff --git a/doc/services.rst b/doc/services.rst new file mode 100644 index 0000000..fd35ec1 --- /dev/null +++ b/doc/services.rst @@ -0,0 +1,264 @@ +Services +======== + +Silex is not only a framework, it is also a service container. It does this by +extending `Pimple `_ which provides a very simple +service container. + +Dependency Injection +-------------------- + +.. note:: + + You can skip this if you already know what Dependency Injection is. + +Dependency Injection is a design pattern where you pass dependencies to +services instead of creating them from within the service or relying on +globals. This generally leads to code that is decoupled, re-usable, flexible +and testable. + +Here is an example of a class that takes a ``User`` object and stores it as a +file in JSON format:: + + class JsonUserPersister + { + private $basePath; + + public function __construct($basePath) + { + $this->basePath = $basePath; + } + + public function persist(User $user) + { + $data = $user->getAttributes(); + $json = json_encode($data); + $filename = $this->basePath.'/'.$user->id.'.json'; + file_put_contents($filename, $json, LOCK_EX); + } + } + +In this simple example the dependency is the ``basePath`` property. It is +passed to the constructor. This means you can create several independent +instances with different base paths. Of course dependencies do not have to be +simple strings. More often they are in fact other services. + +A service container is responsible for creating and storing services. It can +recursively create dependencies of the requested services and inject them. It +does so lazily, which means a service is only created when you actually need it. + +Pimple +------ + +Pimple makes strong use of closures and implements the ArrayAccess interface. + +We will start off by creating a new instance of Pimple -- and because +``Silex\Application`` extends ``Pimple\Container`` all of this applies to Silex +as well:: + + $container = new Pimple\Container(); + +or:: + + $app = new Silex\Application(); + +Parameters +~~~~~~~~~~ + +You can set parameters (which are usually strings) by setting an array key on +the container:: + + $app['some_parameter'] = 'value'; + +The array key can be any value. By convention dots are used for namespacing:: + + $app['asset.host'] = 'http://cdn.mysite.com/'; + +Reading parameter values is possible with the same syntax:: + + echo $app['some_parameter']; + +Service definitions +~~~~~~~~~~~~~~~~~~~ + +Defining services is no different than defining parameters. You just set an +array key on the container to be a closure. However, when you retrieve the +service, the closure is executed. This allows for lazy service creation:: + + $app['some_service'] = function () { + return new Service(); + }; + +And to retrieve the service, use:: + + $service = $app['some_service']; + +On first invocation, this will create the service; the same instance will then +be returned on any subsequent access. + +Factory services +~~~~~~~~~~~~~~~~ + +If you want a different instance to be returned for each service access, wrap +the service definition with the ``factory()`` method:: + + $app['some_service'] = $app->factory(function () { + return new Service(); + }); + +Every time you call ``$app['some_service']``, a new instance of the service is +created. + +Access container from closure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In many cases you will want to access the service container from within a +service definition closure. For example when fetching services the current +service depends on. + +Because of this, the container is passed to the closure as an argument:: + + $app['some_service'] = function ($app) { + return new Service($app['some_other_service'], $app['some_service.config']); + }; + +Here you can see an example of Dependency Injection. ``some_service`` depends +on ``some_other_service`` and takes ``some_service.config`` as configuration +options. The dependency is only created when ``some_service`` is accessed, and +it is possible to replace either of the dependencies by simply overriding +those definitions. + +Going back to our initial example, here's how we could use the container +to manage its dependencies:: + + $app['user.persist_path'] = '/tmp/users'; + $app['user.persister'] = function ($app) { + return new JsonUserPersister($app['user.persist_path']); + }; + + +Protected closures +~~~~~~~~~~~~~~~~~~ + +Because the container sees closures as factories for services, it will always +execute them when reading them. + +In some cases you will however want to store a closure as a parameter, so that +you can fetch it and execute it yourself -- with your own arguments. + +This is why Pimple allows you to protect your closures from being executed, by +using the ``protect`` method:: + + $app['closure_parameter'] = $app->protect(function ($a, $b) { + return $a + $b; + }); + + // will not execute the closure + $add = $app['closure_parameter']; + + // calling it now + echo $add(2, 3); + +Note that protected closures do not get access to the container. + +Core services +------------- + +Silex defines a range of services. + +* **request_stack**: Controls the lifecycle of requests, an instance of + `RequestStack ` _. + It gives you access to ``GET``, ``POST`` parameters and lots more! + + Example usage:: + + $id = $app['request_stack']->getCurrentRequest()->get('id'); + + A request is only available when a request is being served; you can only + access it from within a controller, an application before/after middlewares, + or an error handler. + +* **routes**: The `RouteCollection + `_ + that is used internally. You can add, modify, read routes. + +* **url_generator**: An instance of `UrlGenerator + `_, + using the `RouteCollection + `_ + that is provided through the ``routes`` service. It has a ``generate`` + method, which takes the route name as an argument, followed by an array of + route parameters. + +* **controllers**: The ``Silex\ControllerCollection`` that is used internally. + Check the :doc:`Internals chapter ` for more information. + +* **dispatcher**: The `EventDispatcher + `_ + that is used internally. It is the core of the Symfony system and is used + quite a bit by Silex. + +* **resolver**: The `ControllerResolver + `_ + that is used internally. It takes care of executing the controller with the + right arguments. + +* **kernel**: The `HttpKernel + `_ + that is used internally. The HttpKernel is the heart of Symfony, it takes a + Request as input and returns a Response as output. + +* **request_context**: The request context is a simplified representation of + the request that is used by the router and the URL generator. + +* **exception_handler**: The Exception handler is the default handler that is + used when you don't register one via the ``error()`` method or if your + handler does not return a Response. Disable it with + ``unset($app['exception_handler'])``. + +* **logger**: A `LoggerInterface `_ instance. By default, logging is + disabled as the value is set to ``null``. To enable logging you can either use + the :doc:`MonologServiceProvider ` or define your own ``logger`` service that + conforms to the PSR logger interface. + +Core traits +----------- + +* ``Silex\Application\UrlGeneratorTrait`` adds the following shortcuts: + + * **path**: Generates a path. + + * **url**: Generates an absolute URL. + + .. code-block:: php + + $app->path('homepage'); + $app->url('homepage'); + +Core parameters +--------------- + +* **request.http_port** (optional): Allows you to override the default port + for non-HTTPS URLs. If the current request is HTTP, it will always use the + current port. + + Defaults to 80. + + This parameter can be used when generating URLs. + +* **request.https_port** (optional): Allows you to override the default port + for HTTPS URLs. If the current request is HTTPS, it will always use the + current port. + + Defaults to 443. + + This parameter can be used when generating URLs. + +* **debug** (optional): Returns whether or not the application is running in + debug mode. + + Defaults to false. + +* **charset** (optional): The charset to use for Responses. + + Defaults to UTF-8. diff --git a/doc/testing.rst b/doc/testing.rst new file mode 100644 index 0000000..41054c9 --- /dev/null +++ b/doc/testing.rst @@ -0,0 +1,221 @@ +Testing +======= + +Because Silex is built on top of Symfony, it is very easy to write functional +tests for your application. Functional tests are automated software tests that +ensure that your code is working correctly. They go through the user interface, +using a fake browser, and mimic the actions a user would do. + +Why +--- + +If you are not familiar with software tests, you may be wondering why you would +need this. Every time you make a change to your application, you have to test +it. This means going through all the pages and making sure they are still +working. Functional tests save you a lot of time, because they enable you to +test your application in usually under a second by running a single command. + +For more information on functional testing, unit testing, and automated +software tests in general, check out `PHPUnit +`_ and `Bulat Shakirzyanov's talk +on Clean Code `_. + +PHPUnit +------- + +`PHPUnit `_ is the de-facto +standard testing framework for PHP. It was built for writing unit tests, but it +can be used for functional tests too. You write tests by creating a new class, +that extends the ``PHPUnit_Framework_TestCase``. Your test cases are methods +prefixed with ``test``:: + + class ContactFormTest extends \PHPUnit_Framework_TestCase + { + public function testInitialPage() + { + ... + } + } + +In your test cases, you do assertions on the state of what you are testing. In +this case we are testing a contact form, so we would want to assert that the +page loaded correctly and contains our form:: + + public function testInitialPage() + { + $statusCode = ... + $pageContent = ... + + $this->assertEquals(200, $statusCode); + $this->assertContains('Contact us', $pageContent); + $this->assertContains('`_ +section of the PHPUnit documentation. + +WebTestCase +----------- + +Symfony provides a WebTestCase class that can be used to write functional +tests. The Silex version of this class is ``Silex\WebTestCase``, and you can +use it by making your test extend it:: + + use Silex\WebTestCase; + + class ContactFormTest extends WebTestCase + { + ... + } + +.. caution:: + + If you need to override the ``setUp()`` method, don't forget to call the + parent (``parent::setUp()``) to call the Silex default setup. + +.. note:: + + If you want to use the Symfony ``WebTestCase`` class you will need to + explicitly install its dependencies for your project: + + .. code-block:: bash + + composer require --dev symfony/browser-kit symfony/css-selector + +For your WebTestCase, you will have to implement a ``createApplication`` +method, which returns your application instance:: + + public function createApplication() + { + // app.php must return an Application instance + return require __DIR__.'/path/to/app.php'; + } + +Make sure you do **not** use ``require_once`` here, as this method will be +executed before every test. + +.. tip:: + + By default, the application behaves in the same way as when using it from a + browser. But when an error occurs, it is sometimes easier to get raw + exceptions instead of HTML pages. It is rather simple if you tweak the + application configuration in the ``createApplication()`` method like + follows:: + + public function createApplication() + { + $app = require __DIR__.'/path/to/app.php'; + $app['debug'] = true; + unset($app['exception_handler']); + + return $app; + } + +.. tip:: + + If your application use sessions, set ``session.test`` to ``true`` to + simulate sessions:: + + public function createApplication() + { + // ... + + $app['session.test'] = true; + + // ... + } + +The WebTestCase provides a ``createClient`` method. A client acts as a browser, +and allows you to interact with your application. Here's how it works:: + + public function testInitialPage() + { + $client = $this->createClient(); + $crawler = $client->request('GET', '/'); + + $this->assertTrue($client->getResponse()->isOk()); + $this->assertCount(1, $crawler->filter('h1:contains("Contact us")')); + $this->assertCount(1, $crawler->filter('form')); + ... + } + +There are several things going on here. You have both a ``Client`` and a +``Crawler``. + +You can also access the application through ``$this->app``. + +Client +~~~~~~ + +The client represents a browser. It holds your browsing history, cookies and +more. The ``request`` method allows you to make a request to a page on your +application. + +.. note:: + + You can find some documentation for it in `the client section of the + testing chapter of the Symfony documentation + `_. + +Crawler +~~~~~~~ + +The crawler allows you to inspect the content of a page. You can filter it +using CSS expressions and lots more. + +.. note:: + + You can find some documentation for it in `the crawler section of the testing + chapter of the Symfony documentation + `_. + +Configuration +------------- + +The suggested way to configure PHPUnit is to create a ``phpunit.xml.dist`` +file, a ``tests`` folder and your tests in +``tests/YourApp/Tests/YourTest.php``. The ``phpunit.xml.dist`` file should +look like this: + +.. code-block:: xml + + + + + + ./tests/ + + + + +Your ``tests/YourApp/Tests/YourTest.php`` should look like this:: + + namespace YourApp\Tests; + + use Silex\WebTestCase; + + class YourTest extends WebTestCase + { + public function createApplication() + { + return require __DIR__.'/../../../app.php'; + } + + public function testFooBar() + { + ... + } + } + +Now, when running ``phpunit`` on the command line, tests should run. diff --git a/doc/usage.rst b/doc/usage.rst new file mode 100644 index 0000000..e6a3635 --- /dev/null +++ b/doc/usage.rst @@ -0,0 +1,805 @@ +Usage +===== + +Installation +------------ + +If you want to get started fast, `download`_ Silex as an archive and extract +it, you should have the following directory structure: + +.. code-block:: text + + ├── composer.json + ├── composer.lock + ├── vendor + │ └── ... + └── web + └── index.php + +If you want more flexibility, use Composer_ instead: + +.. code-block:: bash + + composer require silex/silex:~2.0 + +Web Server +---------- + +All examples in the documentation rely on a well-configured web server; read +the :doc:`webserver documentation` to check yours. + +Bootstrap +--------- + +To bootstrap Silex, all you need to do is require the ``vendor/autoload.php`` +file and create an instance of ``Silex\Application``. After your controller +definitions, call the ``run`` method on your application:: + + // web/index.php + require_once __DIR__.'/../vendor/autoload.php'; + + $app = new Silex\Application(); + + // ... definitions + + $app->run(); + +.. tip:: + + When developing a website, you might want to turn on the debug mode to + ease debugging:: + + $app['debug'] = true; + +.. tip:: + + If your application is hosted behind a reverse proxy at address ``$ip``, + and you want Silex to trust the ``X-Forwarded-For*`` headers, you will + need to run your application like this:: + + use Symfony\Component\HttpFoundation\Request; + + Request::setTrustedProxies(array($ip)); + $app->run(); + +Routing +------- + +In Silex you define a route and the controller that is called when that +route is matched. A route pattern consists of: + +* *Pattern*: The route pattern defines a path that points to a resource. The + pattern can include variable parts and you are able to set RegExp + requirements for them. + +* *Method*: One of the following HTTP methods: ``GET``, ``POST``, ``PUT``, + ``DELETE``, ``PATCH``, or ``OPTIONS``. This describes the interaction with + the resource. + +The controller is defined using a closure like this:: + + function () { + // ... do something + } + +The return value of the closure becomes the content of the page. + +Example GET Route +~~~~~~~~~~~~~~~~~ + +Here is an example definition of a ``GET`` route:: + + $blogPosts = array( + 1 => array( + 'date' => '2011-03-29', + 'author' => 'igorw', + 'title' => 'Using Silex', + 'body' => '...', + ), + ); + + $app->get('/blog', function () use ($blogPosts) { + $output = ''; + foreach ($blogPosts as $post) { + $output .= $post['title']; + $output .= '
'; + } + + return $output; + }); + +Visiting ``/blog`` will return a list of blog post titles. The ``use`` +statement means something different in this context. It tells the closure to +import the ``$blogPosts`` variable from the outer scope. This allows you to use +it from within the closure. + +Dynamic Routing +~~~~~~~~~~~~~~~ + +Now, you can create another controller for viewing individual blog posts:: + + $app->get('/blog/{id}', function (Silex\Application $app, $id) use ($blogPosts) { + if (!isset($blogPosts[$id])) { + $app->abort(404, "Post $id does not exist."); + } + + $post = $blogPosts[$id]; + + return "

{$post['title']}

". + "

{$post['body']}

"; + }); + +This route definition has a variable ``{id}`` part which is passed to the +closure. + +The current ``Application`` is automatically injected by Silex to the Closure +thanks to the type hinting. + +When the post does not exist, you are using ``abort()`` to stop the request +early. It actually throws an exception, which you will see how to handle later +on. + +Example POST Route +~~~~~~~~~~~~~~~~~~ + +POST routes signify the creation of a resource. An example for this is a +feedback form. You will use the ``mail`` function to send an e-mail:: + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + $app->post('/feedback', function (Request $request) { + $message = $request->get('message'); + mail('feedback@yoursite.com', '[YourSite] Feedback', $message); + + return new Response('Thank you for your feedback!', 201); + }); + +It is pretty straightforward. + +.. note:: + + There is a :doc:`SwiftmailerServiceProvider ` + included that you can use instead of ``mail()``. + +The current ``request`` is automatically injected by Silex to the Closure +thanks to the type hinting. It is an instance of +Request_, so you can fetch variables using the request ``get`` method. + +Instead of returning a string you are returning an instance of Response_. +This allows setting an HTTP status code, in this case it is set to +``201 Created``. + +.. note:: + + Silex always uses a ``Response`` internally, it converts strings to + responses with status code ``200``. + +Other methods +~~~~~~~~~~~~~ + +You can create controllers for most HTTP methods. Just call one of these +methods on your application: ``get``, ``post``, ``put``, ``delete``, ``patch``, ``options``:: + + $app->put('/blog/{id}', function ($id) { + // ... + }); + + $app->delete('/blog/{id}', function ($id) { + // ... + }); + + $app->patch('/blog/{id}', function ($id) { + // ... + }); + +.. tip:: + + Forms in most web browsers do not directly support the use of other HTTP + methods. To use methods other than GET and POST you can utilize a special + form field with a name of ``_method``. The form's ``method`` attribute must + be set to POST when using this field: + + .. code-block:: html + +
+ + +
+ + You need to explicitly enable this method override:: + + use Symfony\Component\HttpFoundation\Request; + + Request::enableHttpMethodParameterOverride(); + $app->run(); + +You can also call ``match``, which will match all methods. This can be +restricted via the ``method`` method:: + + $app->match('/blog', function () { + // ... + }); + + $app->match('/blog', function () { + // ... + }) + ->method('PATCH'); + + $app->match('/blog', function () { + // ... + }) + ->method('PUT|POST'); + +.. note:: + + The order in which the routes are defined is significant. The first + matching route will be used, so place more generic routes at the bottom. + +Route Variables +~~~~~~~~~~~~~~~ + +As it has been shown before you can define variable parts in a route like +this:: + + $app->get('/blog/{id}', function ($id) { + // ... + }); + +It is also possible to have more than one variable part, just make sure the +closure arguments match the names of the variable parts:: + + $app->get('/blog/{postId}/{commentId}', function ($postId, $commentId) { + // ... + }); + +While it's not recommended, you could also do this (note the switched +arguments):: + + $app->get('/blog/{postId}/{commentId}', function ($commentId, $postId) { + // ... + }); + +You can also ask for the current Request and Application objects:: + + $app->get('/blog/{id}', function (Application $app, Request $request, $id) { + // ... + }); + +.. note:: + + Note for the Application and Request objects, Silex does the injection + based on the type hinting and not on the variable name:: + + $app->get('/blog/{id}', function (Application $foo, Request $bar, $id) { + // ... + }); + +Route Variable Converters +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before injecting the route variables into the controller, you can apply some +converters:: + + $app->get('/user/{id}', function ($id) { + // ... + })->convert('id', function ($id) { return (int) $id; }); + +This is useful when you want to convert route variables to objects as it +allows to reuse the conversion code across different controllers:: + + $userProvider = function ($id) { + return new User($id); + }; + + $app->get('/user/{user}', function (User $user) { + // ... + })->convert('user', $userProvider); + + $app->get('/user/{user}/edit', function (User $user) { + // ... + })->convert('user', $userProvider); + +The converter callback also receives the ``Request`` as its second argument:: + + $callback = function ($post, Request $request) { + return new Post($request->attributes->get('slug')); + }; + + $app->get('/blog/{id}/{slug}', function (Post $post) { + // ... + })->convert('post', $callback); + +A converter can also be defined as a service. For example, here is a user +converter based on Doctrine ObjectManager:: + + use Doctrine\Common\Persistence\ObjectManager; + use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + + class UserConverter + { + private $om; + + public function __construct(ObjectManager $om) + { + $this->om = $om; + } + + public function convert($id) + { + if (null === $user = $this->om->find('User', (int) $id)) { + throw new NotFoundHttpException(sprintf('User %d does not exist', $id)); + } + + return $user; + } + } + +The service will now be registered in the application, and the +``convert()`` method will be used as converter (using the syntax +``service_name:method_name``):: + + $app['converter.user'] = function () { + return new UserConverter(); + }; + + $app->get('/user/{user}', function (User $user) { + // ... + })->convert('user', 'converter.user:convert'); + +Requirements +~~~~~~~~~~~~ + +In some cases you may want to only match certain expressions. You can define +requirements using regular expressions by calling ``assert`` on the +``Controller`` object, which is returned by the routing methods. + +The following will make sure the ``id`` argument is a positive integer, since +``\d+`` matches any amount of digits:: + + $app->get('/blog/{id}', function ($id) { + // ... + }) + ->assert('id', '\d+'); + +You can also chain these calls:: + + $app->get('/blog/{postId}/{commentId}', function ($postId, $commentId) { + // ... + }) + ->assert('postId', '\d+') + ->assert('commentId', '\d+'); + +Conditions +~~~~~~~~~~ + +Besides restricting route matching based on the HTTP method or parameter +requirements, you can set conditions on any part of the request by calling +``when`` on the ``Controller`` object, which is returned by the routing +methods:: + + $app->get('/blog/{id}', function ($id) { + // ... + }) + ->when("request.headers.get('User-Agent') matches '/firefox/i'"); + +The ``when`` argument is a Symfony Expression_ , which means that you need to +add ``symfony/expression-language`` as a dependency of your project. + +Default Values +~~~~~~~~~~~~~~ + +You can define a default value for any route variable by calling ``value`` on +the ``Controller`` object:: + + $app->get('/{pageName}', function ($pageName) { + // ... + }) + ->value('pageName', 'index'); + +This will allow matching ``/``, in which case the ``pageName`` variable will +have the value ``index``. + +Named Routes +~~~~~~~~~~~~ + +Some providers can make use of named routes. By default Silex will generate an +internal route name for you but you can give an explicit route name by calling +``bind``:: + + $app->get('/', function () { + // ... + }) + ->bind('homepage'); + + $app->get('/blog/{id}', function ($id) { + // ... + }) + ->bind('blog_post'); + +Controllers as Classes +~~~~~~~~~~~~~~~~~~~~~~ + +Instead of anonymous functions, you can also define your controllers as +methods. By using the ``ControllerClass::methodName`` syntax, you can tell +Silex to lazily create the controller object for you:: + + $app->get('/', 'Acme\\Foo::bar'); + + use Silex\Application; + use Symfony\Component\HttpFoundation\Request; + + namespace Acme + { + class Foo + { + public function bar(Request $request, Application $app) + { + // ... + } + } + } + +This will load the ``Acme\Foo`` class on demand, create an instance and call +the ``bar`` method to get the response. You can use ``Request`` and +``Silex\Application`` type hints to get ``$request`` and ``$app`` injected. + +It is also possible to :doc:`define your controllers as services +`. + +Global Configuration +-------------------- + +If a controller setting must be applied to **all** controllers (a converter, a +middleware, a requirement, or a default value), configure it on +``$app['controllers']``, which holds all application controllers:: + + $app['controllers'] + ->value('id', '1') + ->assert('id', '\d+') + ->requireHttps() + ->method('get') + ->convert('id', function () { /* ... */ }) + ->before(function () { /* ... */ }) + ->when('request.isSecure() == true') + ; + +These settings are applied to already registered controllers and they become +the defaults for new controllers. + +.. note:: + + The global configuration does not apply to controller providers you might + mount as they have their own global configuration (read the + :doc:`dedicated chapter` for more information). + +Error Handlers +-------------- + +When an exception is thrown, error handlers allow you to display a custom +error page to the user. They can also be used to do additional things, such as +logging. + +To register an error handler, pass a closure to the ``error`` method which +takes an ``Exception`` argument and returns a response:: + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpFoundation\Request; + + $app->error(function (\Exception $e, Request $request, $code) { + return new Response('We are sorry, but something went terribly wrong.'); + }); + +You can also check for specific errors by using the ``$code`` argument, and +handle them differently:: + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpFoundation\Request; + + $app->error(function (\Exception $e, Request $request, $code) { + switch ($code) { + case 404: + $message = 'The requested page could not be found.'; + break; + default: + $message = 'We are sorry, but something went terribly wrong.'; + } + + return new Response($message); + }); + +You can restrict an error handler to only handle some Exception classes by +setting a more specific type hint for the Closure argument:: + + use Symfony\Component\HttpFoundation\Request; + + $app->error(function (\LogicException $e, Request $request, $code) { + // this handler will only handle \LogicException exceptions + // and exceptions that extend \LogicException + }); + +.. note:: + + As Silex ensures that the Response status code is set to the most + appropriate one depending on the exception, setting the status on the + response won't work. If you want to overwrite the status code, set the + ``X-Status-Code`` header:: + + return new Response('Error', 404 /* ignored */, array('X-Status-Code' => 200)); + +If you want to use a separate error handler for logging, make sure you register +it with a higher priority than response error handlers, because once a response +is returned, the following handlers are ignored. + +.. note:: + + Silex ships with a provider for Monolog_ which handles logging of errors. + Check out the *Providers* :doc:`chapter ` for details. + +.. tip:: + + Silex comes with a default error handler that displays a detailed error + message with the stack trace when **debug** is true, and a simple error + message otherwise. Error handlers registered via the ``error()`` method + always take precedence but you can keep the nice error messages when debug + is turned on like this:: + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpFoundation\Request; + + $app->error(function (\Exception $e, Request $request, $code) use ($app) { + if ($app['debug']) { + return; + } + + // ... logic to handle the error and return a Response + }); + +The error handlers are also called when you use ``abort`` to abort a request +early:: + + $app->get('/blog/{id}', function (Silex\Application $app, $id) use ($blogPosts) { + if (!isset($blogPosts[$id])) { + $app->abort(404, "Post $id does not exist."); + } + + return new Response(...); + }); + +You can convert errors to ``Exceptions``, check out the cookbook :doc:`chapter ` for details. + +View Handlers +------------- + +View Handlers allow you to intercept a controller result that is not a +``Response`` and transform it before it gets returned to the kernel. + +To register a view handler, pass a callable (or string that can be resolved to a +callable) to the ``view()`` method. The callable should accept some sort of result +from the controller:: + + $app->view(function (array $controllerResult) use ($app) { + return $app->json($controllerResult); + }); + +View Handlers also receive the ``Request`` as their second argument, +making them a good candidate for basic content negotiation:: + + $app->view(function (array $controllerResult, Request $request) use ($app) { + $acceptHeader = $request->headers->get('Accept'); + $bestFormat = $app['negotiator']->getBestFormat($acceptHeader, array('json', 'xml')); + + if ('json' === $bestFormat) { + return new JsonResponse($controllerResult); + } + + if ('xml' === $bestFormat) { + return $app['serializer.xml']->renderResponse($controllerResult); + } + + return $controllerResult; + }); + +View Handlers will be examined in the order they are added to the application +and Silex will use type hints to determine if a view handler should be used for +the current result, continuously using the return value of the last view handler +as the input for the next. + +.. note:: + + You must ensure that Silex receives a ``Response`` or a string as the result of + the last view handler (or controller) to be run. + +Redirects +--------- + +You can redirect to another page by returning a ``RedirectResponse`` response, +which you can create by calling the ``redirect`` method:: + + $app->get('/', function () use ($app) { + return $app->redirect('/hello'); + }); + +This will redirect from ``/`` to ``/hello``. + +Forwards +-------- + +When you want to delegate the rendering to another controller, without a +round-trip to the browser (as for a redirect), use an internal sub-request:: + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpKernel\HttpKernelInterface; + + $app->get('/', function () use ($app) { + // forward to /hello + $subRequest = Request::create('/hello', 'GET'); + + return $app->handle($subRequest, HttpKernelInterface::SUB_REQUEST); + }); + +.. tip:: + + You can also generate the URI via the built-in URL generator:: + + $request = Request::create($app['url_generator']->generate('hello'), 'GET'); + +There's some more things that you need to keep in mind though. In most cases you +will want to forward some parts of the current master request to the sub-request. +That includes: Cookies, server information, session. +Read more on :doc:`how to make sub-requests `. + +JSON +---- + +If you want to return JSON data, you can use the ``json`` helper method. +Simply pass it your data, status code and headers, and it will create a JSON +response for you:: + + $app->get('/users/{id}', function ($id) use ($app) { + $user = getUser($id); + + if (!$user) { + $error = array('message' => 'The user was not found.'); + + return $app->json($error, 404); + } + + return $app->json($user); + }); + +Streaming +--------- + +It's possible to stream a response, which is important in cases when you don't +want to buffer the data being sent:: + + $app->get('/images/{file}', function ($file) use ($app) { + if (!file_exists(__DIR__.'/images/'.$file)) { + return $app->abort(404, 'The image was not found.'); + } + + $stream = function () use ($file) { + readfile($file); + }; + + return $app->stream($stream, 200, array('Content-Type' => 'image/png')); + }); + +If you need to send chunks, make sure you call ``ob_flush`` and ``flush`` +after every chunk:: + + $stream = function () { + $fh = fopen('http://www.example.com/', 'rb'); + while (!feof($fh)) { + echo fread($fh, 1024); + ob_flush(); + flush(); + } + fclose($fh); + }; + +Sending a file +-------------- + +If you want to return a file, you can use the ``sendFile`` helper method. +It eases returning files that would otherwise not be publicly available. Simply +pass it your file path, status code, headers and the content disposition and it +will create a ``BinaryFileResponse`` response for you:: + + $app->get('/files/{path}', function ($path) use ($app) { + if (!file_exists('/base/path/' . $path)) { + $app->abort(404); + } + + return $app->sendFile('/base/path/' . $path); + }); + +To further customize the response before returning it, check the API doc for +`Symfony\Component\HttpFoundation\BinaryFileResponse +`_:: + + return $app + ->sendFile('/base/path/' . $path) + ->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'pic.jpg') + ; + +Traits +------ + +Silex comes with PHP traits that define shortcut methods. + +Almost all built-in service providers have some corresponding PHP traits. To +use them, define your own Application class and include the traits you want:: + + use Silex\Application; + + class MyApplication extends Application + { + use Application\TwigTrait; + use Application\SecurityTrait; + use Application\FormTrait; + use Application\UrlGeneratorTrait; + use Application\SwiftmailerTrait; + use Application\MonologTrait; + use Application\TranslationTrait; + } + +You can also define your own Route class and use some traits:: + + use Silex\Route; + + class MyRoute extends Route + { + use Route\SecurityTrait; + } + +To use your newly defined route, override the ``$app['route_class']`` +setting:: + + $app['route_class'] = 'MyRoute'; + +Read each provider chapter to learn more about the added methods. + +Security +-------- + +Make sure to protect your application against attacks. + +Escaping +~~~~~~~~ + +When outputting any user input, make sure to escape it correctly to prevent +Cross-Site-Scripting attacks. + +* **Escaping HTML**: PHP provides the ``htmlspecialchars`` function for this. + Silex provides a shortcut ``escape`` method:: + + use Symfony\Component\HttpFoundation\Request; + + $app->get('/name', function (Request $request, Silex\Application $app) { + $name = $request->get('name'); + + return "You provided the name {$app->escape($name)}."; + }); + + If you use the Twig template engine, you should use its escaping or even + auto-escaping mechanisms. Check out the *Providers* :doc:`chapter ` for details. + +* **Escaping JSON**: If you want to provide data in JSON format you should + use the Silex ``json`` function:: + + use Symfony\Component\HttpFoundation\Request; + + $app->get('/name.json', function (Request $request, Silex\Application $app) { + $name = $request->get('name'); + + return $app->json(array('name' => $name)); + }); + +.. _download: http://silex.sensiolabs.org/download +.. _Composer: http://getcomposer.org/ +.. _Request: http://api.symfony.com/master/Symfony/Component/HttpFoundation/Request.html +.. _Response: http://api.symfony.com/master/Symfony/Component/HttpFoundation/Response.html +.. _Monolog: https://github.com/Seldaek/monolog +.. _Expression: https://symfony.com/doc/current/book/routing.html#completely-customized-route-matching-with-conditions diff --git a/doc/web_servers.rst b/doc/web_servers.rst new file mode 100644 index 0000000..cd99faa --- /dev/null +++ b/doc/web_servers.rst @@ -0,0 +1,165 @@ +Webserver Configuration +======================= + +Apache +------ + +If you are using Apache, make sure ``mod_rewrite`` is enabled and use the +following ``.htaccess`` file: + +.. code-block:: apache + + + Options -MultiViews + + RewriteEngine On + #RewriteBase /path/to/app + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [QSA,L] + + +.. note:: + + If your site is not at the webroot level you will have to uncomment the + ``RewriteBase`` statement and adjust the path to point to your directory, + relative from the webroot. + +Alternatively, if you use Apache 2.2.16 or higher, you can use the +`FallbackResource directive`_ to make your .htaccess even easier: + +.. code-block:: apache + + FallbackResource index.php + +.. note:: + + If your site is not at the webroot level you will have to adjust the path to + point to your directory, relative from the webroot. + +nginx +----- + +The **minimum configuration** to get your application running under Nginx is: + +.. code-block:: nginx + + server { + server_name domain.tld www.domain.tld; + root /var/www/project/web; + + location / { + # try to serve file directly, fallback to front controller + try_files $uri /index.php$is_args$args; + } + + # If you have 2 front controllers for dev|prod use the following line instead + # location ~ ^/(index|index_dev)\.php(/|$) { + location ~ ^/index\.php(/|$) { + # the ubuntu default + fastcgi_pass unix:/var/run/php5-fpm.sock; + # for running on centos + #fastcgi_pass unix:/var/run/php-fpm/www.sock; + + fastcgi_split_path_info ^(.+\.php)(/.*)$; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param HTTPS off; + + # Prevents URIs that include the front controller. This will 404: + # http://domain.tld/index.php/some-path + # Enable the internal directive to disable URIs like this + # internal; + } + + #return 404 for all php files as we do have a front controller + location ~ \.php$ { + return 404; + } + + error_log /var/log/nginx/project_error.log; + access_log /var/log/nginx/project_access.log; + } + +IIS +--- + +If you are using the Internet Information Services from Windows, you can use +this sample ``web.config`` file: + +.. code-block:: xml + + + + + + + + + + + + + + + + + + + + + + + + +Lighttpd +-------- + +If you are using lighttpd, use this sample ``simple-vhost`` as a starting +point: + +.. code-block:: lighttpd + + server.document-root = "/path/to/app" + + url.rewrite-once = ( + # configure some static files + "^/assets/.+" => "$0", + "^/favicon\.ico$" => "$0", + + "^(/[^\?]*)(\?.*)?" => "/index.php$1$2" + ) + +.. _FallbackResource directive: http://www.adayinthelifeof.nl/2012/01/21/apaches-fallbackresource-your-new-htaccess-command/ + +PHP +--- + +PHP ships with a built-in webserver for development. This server allows you to +run silex without any configuration. However, in order to serve static files, +you'll have to make sure your front controller returns false in that case:: + + // web/index.php + + $filename = __DIR__.preg_replace('#(\?.*)$#', '', $_SERVER['REQUEST_URI']); + if (php_sapi_name() === 'cli-server' && is_file($filename)) { + return false; + } + + $app = require __DIR__.'/../src/app.php'; + $app->run(); + + +Assuming your front controller is at ``web/index.php``, you can start the +server from the command-line with this command: + +.. code-block:: text + + $ php -S localhost:8080 -t web web/index.php + +Now the application should be running at ``http://localhost:8080``. + +.. note:: + + This server is for development only. It is **not** recommended to use it + in production. diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..799f16c --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,24 @@ + + + + + + ./tests/Silex/ + + + + + ./src + + + diff --git a/src/Silex/Api/BootableProviderInterface.php b/src/Silex/Api/BootableProviderInterface.php new file mode 100644 index 0000000..739e04d --- /dev/null +++ b/src/Silex/Api/BootableProviderInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Api; + +use Silex\Application; + +/** + * Interface for bootable service providers. + * + * @author Fabien Potencier + */ +interface BootableProviderInterface +{ + /** + * Bootstraps the application. + * + * This method is called after all services are registered + * and should be used for "dynamic" configuration (whenever + * a service must be requested). + * + * @param Application $app + */ + public function boot(Application $app); +} diff --git a/src/Silex/Api/ControllerProviderInterface.php b/src/Silex/Api/ControllerProviderInterface.php new file mode 100644 index 0000000..28d9d0e --- /dev/null +++ b/src/Silex/Api/ControllerProviderInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Api; + +use Silex\Application; +use Silex\ControllerCollection; + +/** + * Interface for controller providers. + * + * @author Fabien Potencier + */ +interface ControllerProviderInterface +{ + /** + * Returns routes to connect to the given application. + * + * @param Application $app An Application instance + * + * @return ControllerCollection A ControllerCollection instance + */ + public function connect(Application $app); +} diff --git a/src/Silex/Api/EventListenerProviderInterface.php b/src/Silex/Api/EventListenerProviderInterface.php new file mode 100644 index 0000000..f3e6255 --- /dev/null +++ b/src/Silex/Api/EventListenerProviderInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Api; + +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Pimple\Container; + +/** + * Interface for event listener providers. + * + * @author Fabien Potencier + */ +interface EventListenerProviderInterface +{ + public function subscribe(Container $app, EventDispatcherInterface $dispatcher); +} diff --git a/src/Silex/Api/LICENSE b/src/Silex/Api/LICENSE new file mode 100644 index 0000000..bc6ad04 --- /dev/null +++ b/src/Silex/Api/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010-2015 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Silex/Api/composer.json b/src/Silex/Api/composer.json new file mode 100644 index 0000000..da53e8f --- /dev/null +++ b/src/Silex/Api/composer.json @@ -0,0 +1,34 @@ +{ + "minimum-stability": "dev", + "name": "silex/api", + "description": "The Silex interfaces", + "keywords": ["microframework"], + "homepage": "http://silex.sensiolabs.org", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "require": { + "php": ">=5.5.9", + "pimple/pimple": "~3.0" + }, + "suggest": { + "symfony/event-dispatcher": "For EventListenerProviderInterface", + "silex/silex": "For BootableProviderInterface and ControllerProviderInterface" + }, + "autoload": { + "psr-4": { "Silex\\Api\\": "" } + }, + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + } +} diff --git a/src/Silex/AppArgumentValueResolver.php b/src/Silex/AppArgumentValueResolver.php new file mode 100644 index 0000000..d6449b4 --- /dev/null +++ b/src/Silex/AppArgumentValueResolver.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; + +/** + * HttpKernel Argument Resolver for Silex. + * + * @author Romain Neutron + */ +class AppArgumentValueResolver implements ArgumentValueResolverInterface +{ + private $app; + + public function __construct(Application $app) + { + $this->app = $app; + } + + /** + * {@inheritdoc} + */ + public function supports(Request $request, ArgumentMetadata $argument) + { + return $argument->getType() === Application::class || (null !== $argument->getType() && in_array(Application::class, class_parents($argument->getType()), true)); + } + + /** + * {@inheritdoc} + */ + public function resolve(Request $request, ArgumentMetadata $argument) + { + yield $this->app; + } +} diff --git a/src/Silex/Application.php b/src/Silex/Application.php new file mode 100644 index 0000000..90b7d00 --- /dev/null +++ b/src/Silex/Application.php @@ -0,0 +1,506 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\TerminableInterface; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Event\PostResponseEvent; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpFoundation\JsonResponse; +use Silex\Api\BootableProviderInterface; +use Silex\Api\EventListenerProviderInterface; +use Silex\Api\ControllerProviderInterface; +use Silex\Provider\ExceptionHandlerServiceProvider; +use Silex\Provider\RoutingServiceProvider; +use Silex\Provider\HttpKernelServiceProvider; + +/** + * The Silex framework class. + * + * @author Fabien Potencier + */ +class Application extends Container implements HttpKernelInterface, TerminableInterface +{ + const VERSION = '2.0.2'; + + const EARLY_EVENT = 512; + const LATE_EVENT = -512; + + protected $providers = array(); + protected $booted = false; + + /** + * Instantiate a new Application. + * + * Objects and parameters can be passed as argument to the constructor. + * + * @param array $values The parameters or objects. + */ + public function __construct(array $values = array()) + { + parent::__construct(); + + $this['request.http_port'] = 80; + $this['request.https_port'] = 443; + $this['debug'] = false; + $this['charset'] = 'UTF-8'; + $this['logger'] = null; + + $this->register(new HttpKernelServiceProvider()); + $this->register(new RoutingServiceProvider()); + $this->register(new ExceptionHandlerServiceProvider()); + + foreach ($values as $key => $value) { + $this[$key] = $value; + } + } + + /** + * Registers a service provider. + * + * @param ServiceProviderInterface $provider A ServiceProviderInterface instance + * @param array $values An array of values that customizes the provider + * + * @return Application + */ + public function register(ServiceProviderInterface $provider, array $values = array()) + { + $this->providers[] = $provider; + + parent::register($provider, $values); + + return $this; + } + + /** + * Boots all service providers. + * + * This method is automatically called by handle(), but you can use it + * to boot all service providers when not handling a request. + */ + public function boot() + { + if ($this->booted) { + return; + } + + $this->booted = true; + + foreach ($this->providers as $provider) { + if ($provider instanceof EventListenerProviderInterface) { + $provider->subscribe($this, $this['dispatcher']); + } + + if ($provider instanceof BootableProviderInterface) { + $provider->boot($this); + } + } + } + + /** + * Maps a pattern to a callable. + * + * You can optionally specify HTTP methods that should be matched. + * + * @param string $pattern Matched route pattern + * @param mixed $to Callback that returns the response when matched + * + * @return Controller + */ + public function match($pattern, $to = null) + { + return $this['controllers']->match($pattern, $to); + } + + /** + * Maps a GET request to a callable. + * + * @param string $pattern Matched route pattern + * @param mixed $to Callback that returns the response when matched + * + * @return Controller + */ + public function get($pattern, $to = null) + { + return $this['controllers']->get($pattern, $to); + } + + /** + * Maps a POST request to a callable. + * + * @param string $pattern Matched route pattern + * @param mixed $to Callback that returns the response when matched + * + * @return Controller + */ + public function post($pattern, $to = null) + { + return $this['controllers']->post($pattern, $to); + } + + /** + * Maps a PUT request to a callable. + * + * @param string $pattern Matched route pattern + * @param mixed $to Callback that returns the response when matched + * + * @return Controller + */ + public function put($pattern, $to = null) + { + return $this['controllers']->put($pattern, $to); + } + + /** + * Maps a DELETE request to a callable. + * + * @param string $pattern Matched route pattern + * @param mixed $to Callback that returns the response when matched + * + * @return Controller + */ + public function delete($pattern, $to = null) + { + return $this['controllers']->delete($pattern, $to); + } + + /** + * Maps an OPTIONS request to a callable. + * + * @param string $pattern Matched route pattern + * @param mixed $to Callback that returns the response when matched + * + * @return Controller + */ + public function options($pattern, $to = null) + { + return $this['controllers']->options($pattern, $to); + } + + /** + * Maps a PATCH request to a callable. + * + * @param string $pattern Matched route pattern + * @param mixed $to Callback that returns the response when matched + * + * @return Controller + */ + public function patch($pattern, $to = null) + { + return $this['controllers']->patch($pattern, $to); + } + + /** + * Adds an event listener that listens on the specified events. + * + * @param string $eventName The event to listen on + * @param callable $callback The listener + * @param int $priority The higher this value, the earlier an event + * listener will be triggered in the chain (defaults to 0) + */ + public function on($eventName, $callback, $priority = 0) + { + if ($this->booted) { + $this['dispatcher']->addListener($eventName, $this['callback_resolver']->resolveCallback($callback), $priority); + + return; + } + + $this->extend('dispatcher', function (EventDispatcherInterface $dispatcher, $app) use ($callback, $priority, $eventName) { + $dispatcher->addListener($eventName, $app['callback_resolver']->resolveCallback($callback), $priority); + + return $dispatcher; + }); + } + + /** + * Registers a before filter. + * + * Before filters are run before any route has been matched. + * + * @param mixed $callback Before filter callback + * @param int $priority The higher this value, the earlier an event + * listener will be triggered in the chain (defaults to 0) + */ + public function before($callback, $priority = 0) + { + $app = $this; + + $this->on(KernelEvents::REQUEST, function (GetResponseEvent $event) use ($callback, $app) { + if (!$event->isMasterRequest()) { + return; + } + + $ret = call_user_func($app['callback_resolver']->resolveCallback($callback), $event->getRequest(), $app); + + if ($ret instanceof Response) { + $event->setResponse($ret); + } + }, $priority); + } + + /** + * Registers an after filter. + * + * After filters are run after the controller has been executed. + * + * @param mixed $callback After filter callback + * @param int $priority The higher this value, the earlier an event + * listener will be triggered in the chain (defaults to 0) + */ + public function after($callback, $priority = 0) + { + $app = $this; + + $this->on(KernelEvents::RESPONSE, function (FilterResponseEvent $event) use ($callback, $app) { + if (!$event->isMasterRequest()) { + return; + } + + $response = call_user_func($app['callback_resolver']->resolveCallback($callback), $event->getRequest(), $event->getResponse(), $app); + if ($response instanceof Response) { + $event->setResponse($response); + } elseif (null !== $response) { + throw new \RuntimeException('An after middleware returned an invalid response value. Must return null or an instance of Response.'); + } + }, $priority); + } + + /** + * Registers a finish filter. + * + * Finish filters are run after the response has been sent. + * + * @param mixed $callback Finish filter callback + * @param int $priority The higher this value, the earlier an event + * listener will be triggered in the chain (defaults to 0) + */ + public function finish($callback, $priority = 0) + { + $app = $this; + + $this->on(KernelEvents::TERMINATE, function (PostResponseEvent $event) use ($callback, $app) { + call_user_func($app['callback_resolver']->resolveCallback($callback), $event->getRequest(), $event->getResponse(), $app); + }, $priority); + } + + /** + * Aborts the current request by sending a proper HTTP error. + * + * @param int $statusCode The HTTP status code + * @param string $message The status message + * @param array $headers An array of HTTP headers + */ + public function abort($statusCode, $message = '', array $headers = array()) + { + throw new HttpException($statusCode, $message, null, $headers); + } + + /** + * Registers an error handler. + * + * Error handlers are simple callables which take a single Exception + * as an argument. If a controller throws an exception, an error handler + * can return a specific response. + * + * When an exception occurs, all handlers will be called, until one returns + * something (a string or a Response object), at which point that will be + * returned to the client. + * + * For this reason you should add logging handlers before output handlers. + * + * @param mixed $callback Error handler callback, takes an Exception argument + * @param int $priority The higher this value, the earlier an event + * listener will be triggered in the chain (defaults to -8) + */ + public function error($callback, $priority = -8) + { + $this->on(KernelEvents::EXCEPTION, new ExceptionListenerWrapper($this, $callback), $priority); + } + + /** + * Registers a view handler. + * + * View handlers are simple callables which take a controller result and the + * request as arguments, whenever a controller returns a value that is not + * an instance of Response. When this occurs, all suitable handlers will be + * called, until one returns a Response object. + * + * @param mixed $callback View handler callback + * @param int $priority The higher this value, the earlier an event + * listener will be triggered in the chain (defaults to 0) + */ + public function view($callback, $priority = 0) + { + $this->on(KernelEvents::VIEW, new ViewListenerWrapper($this, $callback), $priority); + } + + /** + * Flushes the controller collection. + */ + public function flush() + { + $this['routes']->addCollection($this['controllers']->flush()); + } + + /** + * Redirects the user to another URL. + * + * @param string $url The URL to redirect to + * @param int $status The status code (302 by default) + * + * @return RedirectResponse + */ + public function redirect($url, $status = 302) + { + return new RedirectResponse($url, $status); + } + + /** + * Creates a streaming response. + * + * @param mixed $callback A valid PHP callback + * @param int $status The response status code + * @param array $headers An array of response headers + * + * @return StreamedResponse + */ + public function stream($callback = null, $status = 200, array $headers = array()) + { + return new StreamedResponse($callback, $status, $headers); + } + + /** + * Escapes a text for HTML. + * + * @param string $text The input text to be escaped + * @param int $flags The flags (@see htmlspecialchars) + * @param string $charset The charset + * @param bool $doubleEncode Whether to try to avoid double escaping or not + * + * @return string Escaped text + */ + public function escape($text, $flags = ENT_COMPAT, $charset = null, $doubleEncode = true) + { + return htmlspecialchars($text, $flags, $charset ?: $this['charset'], $doubleEncode); + } + + /** + * Convert some data into a JSON response. + * + * @param mixed $data The response data + * @param int $status The response status code + * @param array $headers An array of response headers + * + * @return JsonResponse + */ + public function json($data = array(), $status = 200, array $headers = array()) + { + return new JsonResponse($data, $status, $headers); + } + + /** + * Sends a file. + * + * @param \SplFileInfo|string $file The file to stream + * @param int $status The response status code + * @param array $headers An array of response headers + * @param null|string $contentDisposition The type of Content-Disposition to set automatically with the filename + * + * @return BinaryFileResponse + */ + public function sendFile($file, $status = 200, array $headers = array(), $contentDisposition = null) + { + return new BinaryFileResponse($file, $status, $headers, true, $contentDisposition); + } + + /** + * Mounts controllers under the given route prefix. + * + * @param string $prefix The route prefix + * @param ControllerCollection|callable|ControllerProviderInterface $controllers A ControllerCollection, a callable, or a ControllerProviderInterface instance + * + * @return Application + * + * @throws \LogicException + */ + public function mount($prefix, $controllers) + { + if ($controllers instanceof ControllerProviderInterface) { + $connectedControllers = $controllers->connect($this); + + if (!$connectedControllers instanceof ControllerCollection) { + throw new \LogicException(sprintf('The method "%s::connect" must return a "ControllerCollection" instance. Got: "%s"', get_class($controllers), is_object($connectedControllers) ? get_class($connectedControllers) : gettype($connectedControllers))); + } + + $controllers = $connectedControllers; + } elseif (!$controllers instanceof ControllerCollection && !is_callable($controllers)) { + throw new \LogicException('The "mount" method takes either a "ControllerCollection" instance, "ControllerProviderInterface" instance, or a callable.'); + } + + $this['controllers']->mount($prefix, $controllers); + + return $this; + } + + /** + * Handles the request and delivers the response. + * + * @param Request|null $request Request to process + */ + public function run(Request $request = null) + { + if (null === $request) { + $request = Request::createFromGlobals(); + } + + $response = $this->handle($request); + $response->send(); + $this->terminate($request, $response); + } + + /** + * {@inheritdoc} + * + * If you call this method directly instead of run(), you must call the + * terminate() method yourself if you want the finish filters to be run. + */ + public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) + { + if (!$this->booted) { + $this->boot(); + } + + $this->flush(); + + return $this['kernel']->handle($request, $type, $catch); + } + + /** + * {@inheritdoc} + */ + public function terminate(Request $request, Response $response) + { + $this['kernel']->terminate($request, $response); + } +} diff --git a/src/Silex/Application/FormTrait.php b/src/Silex/Application/FormTrait.php new file mode 100644 index 0000000..2eeb23e --- /dev/null +++ b/src/Silex/Application/FormTrait.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Application; + +use Symfony\Component\Form; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\FormBuilder; +use Symfony\Component\OptionsResolver\OptionsResolver\FormTypeInterface; + +/** + * Form trait. + * + * @author Fabien Potencier + * @author David Berlioz + */ +trait FormTrait +{ + /** + * Creates and returns a form builder instance. + * + * @param mixed $data The initial data for the form + * @param array $options Options for the form + * @param string|FormTypeInterface $type Type of the form + * + * @return FormBuilder + */ + public function form($data = null, array $options = array(), $type = null) + { + return $this['form.factory']->createBuilder($type ?: FormType::class, $data, $options); + } + + /** + * Creates and returns a named form builder instance. + * + * @param string $name + * @param mixed $data The initial data for the form + * @param array $options Options for the form + * @param string|FormTypeInterface $type Type of the form + * + * @return FormBuilder + */ + public function namedForm($name, $data = null, array $options = array(), $type = null) + { + return $this['form.factory']->createNamedBuilder($name, $type ?: FormType::class, $data, $options); + } +} diff --git a/src/Silex/Application/MonologTrait.php b/src/Silex/Application/MonologTrait.php new file mode 100644 index 0000000..18cb54c --- /dev/null +++ b/src/Silex/Application/MonologTrait.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Application; + +use Monolog\Logger; + +/** + * Monolog trait. + * + * @author Fabien Potencier + */ +trait MonologTrait +{ + /** + * Adds a log record. + * + * @param string $message The log message + * @param array $context The log context + * @param int $level The logging level + * + * @return bool Whether the record has been processed + */ + public function log($message, array $context = array(), $level = Logger::INFO) + { + return $this['monolog']->addRecord($level, $message, $context); + } +} diff --git a/src/Silex/Application/SecurityTrait.php b/src/Silex/Application/SecurityTrait.php new file mode 100644 index 0000000..43ce555 --- /dev/null +++ b/src/Silex/Application/SecurityTrait.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Application; + +use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException; +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * Security trait. + * + * @author Fabien Potencier + */ +trait SecurityTrait +{ + /** + * Encodes the raw password. + * + * @param UserInterface $user A UserInterface instance + * @param string $password The password to encode + * + * @return string The encoded password + * + * @throws \RuntimeException when no password encoder could be found for the user + */ + public function encodePassword(UserInterface $user, $password) + { + return $this['security.encoder_factory']->getEncoder($user)->encodePassword($password, $user->getSalt()); + } + + /** + * Checks if the attributes are granted against the current authentication token and optionally supplied object. + * + * @param mixed $attributes + * @param mixed $object + * + * @return bool + * + * @throws AuthenticationCredentialsNotFoundException when the token storage has no authentication token. + */ + public function isGranted($attributes, $object = null) + { + return $this['security.authorization_checker']->isGranted($attributes, $object); + } +} diff --git a/src/Silex/Application/SwiftmailerTrait.php b/src/Silex/Application/SwiftmailerTrait.php new file mode 100644 index 0000000..157f94d --- /dev/null +++ b/src/Silex/Application/SwiftmailerTrait.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Application; + +/** + * Swiftmailer trait. + * + * @author Fabien Potencier + */ +trait SwiftmailerTrait +{ + /** + * Sends an email. + * + * @param \Swift_Message $message A \Swift_Message instance + * @param array $failedRecipients An array of failures by-reference + * + * @return int The number of sent messages + */ + public function mail(\Swift_Message $message, &$failedRecipients = null) + { + return $this['mailer']->send($message, $failedRecipients); + } +} diff --git a/src/Silex/Application/TranslationTrait.php b/src/Silex/Application/TranslationTrait.php new file mode 100644 index 0000000..8b6e818 --- /dev/null +++ b/src/Silex/Application/TranslationTrait.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Application; + +/** + * Translation trait. + * + * @author Fabien Potencier + */ +trait TranslationTrait +{ + /** + * Translates the given message. + * + * @param string $id The message id + * @param array $parameters An array of parameters for the message + * @param string $domain The domain for the message + * @param string $locale The locale + * + * @return string The translated string + */ + public function trans($id, array $parameters = array(), $domain = 'messages', $locale = null) + { + return $this['translator']->trans($id, $parameters, $domain, $locale); + } + + /** + * Translates the given choice message by choosing a translation according to a number. + * + * @param string $id The message id + * @param int $number The number to use to find the indice of the message + * @param array $parameters An array of parameters for the message + * @param string $domain The domain for the message + * @param string $locale The locale + * + * @return string The translated string + */ + public function transChoice($id, $number, array $parameters = array(), $domain = 'messages', $locale = null) + { + return $this['translator']->transChoice($id, $number, $parameters, $domain, $locale); + } +} diff --git a/src/Silex/Application/TwigTrait.php b/src/Silex/Application/TwigTrait.php new file mode 100644 index 0000000..cb4127d --- /dev/null +++ b/src/Silex/Application/TwigTrait.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Application; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedResponse; + +/** + * Twig trait. + * + * @author Fabien Potencier + */ +trait TwigTrait +{ + /** + * Renders a view and returns a Response. + * + * To stream a view, pass an instance of StreamedResponse as a third argument. + * + * @param string $view The view name + * @param array $parameters An array of parameters to pass to the view + * @param Response $response A Response instance + * + * @return Response A Response instance + */ + public function render($view, array $parameters = array(), Response $response = null) + { + $twig = $this['twig']; + + if ($response instanceof StreamedResponse) { + $response->setCallback(function () use ($twig, $view, $parameters) { + $twig->display($view, $parameters); + }); + } else { + if (null === $response) { + $response = new Response(); + } + $response->setContent($twig->render($view, $parameters)); + } + + return $response; + } + + /** + * Renders a view. + * + * @param string $view The view name + * @param array $parameters An array of parameters to pass to the view + * + * @return string The rendered view + */ + public function renderView($view, array $parameters = array()) + { + return $this['twig']->render($view, $parameters); + } +} diff --git a/src/Silex/Application/UrlGeneratorTrait.php b/src/Silex/Application/UrlGeneratorTrait.php new file mode 100644 index 0000000..7ccdf8a --- /dev/null +++ b/src/Silex/Application/UrlGeneratorTrait.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Application; + +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + +/** + * UrlGenerator trait. + * + * @author Fabien Potencier + */ +trait UrlGeneratorTrait +{ + /** + * Generates a path from the given parameters. + * + * @param string $route The name of the route + * @param mixed $parameters An array of parameters + * + * @return string The generated path + */ + public function path($route, $parameters = array()) + { + return $this['url_generator']->generate($route, $parameters, UrlGeneratorInterface::ABSOLUTE_PATH); + } + + /** + * Generates an absolute URL from the given parameters. + * + * @param string $route The name of the route + * @param mixed $parameters An array of parameters + * + * @return string The generated URL + */ + public function url($route, $parameters = array()) + { + return $this['url_generator']->generate($route, $parameters, UrlGeneratorInterface::ABSOLUTE_URL); + } +} diff --git a/src/Silex/CallbackResolver.php b/src/Silex/CallbackResolver.php new file mode 100644 index 0000000..692901c --- /dev/null +++ b/src/Silex/CallbackResolver.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex; + +use Pimple\Container; + +class CallbackResolver +{ + const SERVICE_PATTERN = "/[A-Za-z0-9\._\-]+:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/"; + + private $app; + + public function __construct(Container $app) + { + $this->app = $app; + } + + /** + * Returns true if the string is a valid service method representation. + * + * @param string $name + * + * @return bool + */ + public function isValid($name) + { + return is_string($name) && (preg_match(static::SERVICE_PATTERN, $name) || isset($this->app[$name])); + } + + /** + * Returns a callable given its string representation. + * + * @param string $name + * + * @return callable + * + * @throws \InvalidArgumentException In case the method does not exist. + */ + public function convertCallback($name) + { + if (preg_match(static::SERVICE_PATTERN, $name)) { + list($service, $method) = explode(':', $name, 2); + $callback = array($this->app[$service], $method); + } else { + $service = $name; + $callback = $this->app[$name]; + } + + if (!is_callable($callback)) { + throw new \InvalidArgumentException(sprintf('Service "%s" is not callable.', $service)); + } + + return $callback; + } + + /** + * Returns a callable given its string representation if it is a valid service method. + * + * @param string $name + * + * @return string|callable A callable value or the string passed in + * + * @throws \InvalidArgumentException In case the method does not exist. + */ + public function resolveCallback($name) + { + return $this->isValid($name) ? $this->convertCallback($name) : $name; + } +} diff --git a/src/Silex/Controller.php b/src/Silex/Controller.php new file mode 100644 index 0000000..9a80755 --- /dev/null +++ b/src/Silex/Controller.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex; + +use Silex\Exception\ControllerFrozenException; + +/** + * A wrapper for a controller, mapped to a route. + * + * __call() forwards method-calls to Route, but returns instance of Controller + * listing Route's methods below, so that IDEs know they are valid + * + * @method Controller assert(string $variable, string $regexp) + * @method Controller value(string $variable, mixed $default) + * @method Controller convert(string $variable, mixed $callback) + * @method Controller method(string $method) + * @method Controller requireHttp() + * @method Controller requireHttps() + * @method Controller before(mixed $callback) + * @method Controller after(mixed $callback) + * @method Controller when(string $condition) + * + * @author Igor Wiedler + */ +class Controller +{ + private $route; + private $routeName; + private $isFrozen = false; + + /** + * Constructor. + * + * @param Route $route + */ + public function __construct(Route $route) + { + $this->route = $route; + } + + /** + * Gets the controller's route. + * + * @return Route + */ + public function getRoute() + { + return $this->route; + } + + /** + * Gets the controller's route name. + * + * @return string + */ + public function getRouteName() + { + return $this->routeName; + } + + /** + * Sets the controller's route. + * + * @param string $routeName + * + * @return Controller $this The current Controller instance + */ + public function bind($routeName) + { + if ($this->isFrozen) { + throw new ControllerFrozenException(sprintf('Calling %s on frozen %s instance.', __METHOD__, __CLASS__)); + } + + $this->routeName = $routeName; + + return $this; + } + + public function __call($method, $arguments) + { + if (!method_exists($this->route, $method)) { + throw new \BadMethodCallException(sprintf('Method "%s::%s" does not exist.', get_class($this->route), $method)); + } + + call_user_func_array(array($this->route, $method), $arguments); + + return $this; + } + + /** + * Freezes the controller. + * + * Once the controller is frozen, you can no longer change the route name + */ + public function freeze() + { + $this->isFrozen = true; + } + + public function generateRouteName($prefix) + { + $methods = implode('_', $this->route->getMethods()).'_'; + + $routeName = $methods.$prefix.$this->route->getPath(); + $routeName = str_replace(array('/', ':', '|', '-'), '_', $routeName); + $routeName = preg_replace('/[^a-z0-9A-Z_.]+/', '', $routeName); + + // Collapse consecutive underscores down into a single underscore. + $routeName = preg_replace('/_+/', '_', $routeName); + + return $routeName; + } +} diff --git a/src/Silex/ControllerCollection.php b/src/Silex/ControllerCollection.php new file mode 100644 index 0000000..4036896 --- /dev/null +++ b/src/Silex/ControllerCollection.php @@ -0,0 +1,239 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex; + +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\HttpFoundation\Request; + +/** + * Builds Silex controllers. + * + * It acts as a staging area for routes. You are able to set the route name + * until flush() is called, at which point all controllers are frozen and + * converted to a RouteCollection. + * + * __call() forwards method-calls to Route, but returns instance of ControllerCollection + * listing Route's methods below, so that IDEs know they are valid + * + * @method ControllerCollection assert(string $variable, string $regexp) + * @method ControllerCollection value(string $variable, mixed $default) + * @method ControllerCollection convert(string $variable, mixed $callback) + * @method ControllerCollection method(string $method) + * @method ControllerCollection requireHttp() + * @method ControllerCollection requireHttps() + * @method ControllerCollection before(mixed $callback) + * @method ControllerCollection after(mixed $callback) + * @method ControllerCollection when(string $condition) + * + * @author Igor Wiedler + * @author Fabien Potencier + */ +class ControllerCollection +{ + protected $controllers = array(); + protected $defaultRoute; + protected $defaultController; + protected $prefix; + protected $routesFactory; + protected $controllersFactory; + + public function __construct(Route $defaultRoute, RouteCollection $routesFactory = null, $controllersFactory = null) + { + $this->defaultRoute = $defaultRoute; + $this->routesFactory = $routesFactory; + $this->controllersFactory = $controllersFactory; + $this->defaultController = function (Request $request) { + throw new \LogicException(sprintf('The "%s" route must have code to run when it matches.', $request->attributes->get('_route'))); + }; + } + + /** + * Mounts controllers under the given route prefix. + * + * @param string $prefix The route prefix + * @param ControllerCollection|callable $controllers A ControllerCollection instance or a callable for defining routes + * + * @throws \LogicException + */ + public function mount($prefix, $controllers) + { + if (is_callable($controllers)) { + $collection = $this->controllersFactory ? call_user_func($this->controllersFactory) : new static(new Route(), new RouteCollection()); + call_user_func($controllers, $collection); + $controllers = $collection; + } elseif (!$controllers instanceof self) { + throw new \LogicException('The "mount" method takes either a "ControllerCollection" instance or callable.'); + } + + $controllers->prefix = $prefix; + + $this->controllers[] = $controllers; + } + + /** + * Maps a pattern to a callable. + * + * You can optionally specify HTTP methods that should be matched. + * + * @param string $pattern Matched route pattern + * @param mixed $to Callback that returns the response when matched + * + * @return Controller + */ + public function match($pattern, $to = null) + { + $route = clone $this->defaultRoute; + $route->setPath($pattern); + $this->controllers[] = $controller = new Controller($route); + $route->setDefault('_controller', null === $to ? $this->defaultController : $to); + + return $controller; + } + + /** + * Maps a GET request to a callable. + * + * @param string $pattern Matched route pattern + * @param mixed $to Callback that returns the response when matched + * + * @return Controller + */ + public function get($pattern, $to = null) + { + return $this->match($pattern, $to)->method('GET'); + } + + /** + * Maps a POST request to a callable. + * + * @param string $pattern Matched route pattern + * @param mixed $to Callback that returns the response when matched + * + * @return Controller + */ + public function post($pattern, $to = null) + { + return $this->match($pattern, $to)->method('POST'); + } + + /** + * Maps a PUT request to a callable. + * + * @param string $pattern Matched route pattern + * @param mixed $to Callback that returns the response when matched + * + * @return Controller + */ + public function put($pattern, $to = null) + { + return $this->match($pattern, $to)->method('PUT'); + } + + /** + * Maps a DELETE request to a callable. + * + * @param string $pattern Matched route pattern + * @param mixed $to Callback that returns the response when matched + * + * @return Controller + */ + public function delete($pattern, $to = null) + { + return $this->match($pattern, $to)->method('DELETE'); + } + + /** + * Maps an OPTIONS request to a callable. + * + * @param string $pattern Matched route pattern + * @param mixed $to Callback that returns the response when matched + * + * @return Controller + */ + public function options($pattern, $to = null) + { + return $this->match($pattern, $to)->method('OPTIONS'); + } + + /** + * Maps a PATCH request to a callable. + * + * @param string $pattern Matched route pattern + * @param mixed $to Callback that returns the response when matched + * + * @return Controller + */ + public function patch($pattern, $to = null) + { + return $this->match($pattern, $to)->method('PATCH'); + } + + public function __call($method, $arguments) + { + if (!method_exists($this->defaultRoute, $method)) { + throw new \BadMethodCallException(sprintf('Method "%s::%s" does not exist.', get_class($this->defaultRoute), $method)); + } + + call_user_func_array(array($this->defaultRoute, $method), $arguments); + + foreach ($this->controllers as $controller) { + call_user_func_array(array($controller, $method), $arguments); + } + + return $this; + } + + /** + * Persists and freezes staged controllers. + * + * @return RouteCollection A RouteCollection instance + */ + public function flush() + { + if (null === $this->routesFactory) { + $routes = new RouteCollection(); + } else { + $routes = $this->routesFactory; + } + + return $this->doFlush('', $routes); + } + + private function doFlush($prefix, RouteCollection $routes) + { + if ($prefix !== '') { + $prefix = '/'.trim(trim($prefix), '/'); + } + + foreach ($this->controllers as $controller) { + if ($controller instanceof Controller) { + $controller->getRoute()->setPath($prefix.$controller->getRoute()->getPath()); + if (!$name = $controller->getRouteName()) { + $name = $base = $controller->generateRouteName(''); + $i = 0; + while ($routes->get($name)) { + $name = $base.'_'.++$i; + } + $controller->bind($name); + } + $routes->add($name, $controller->getRoute()); + $controller->freeze(); + } else { + $controller->doFlush($prefix.$controller->prefix, $routes); + } + } + + $this->controllers = array(); + + return $routes; + } +} diff --git a/src/Silex/ControllerResolver.php b/src/Silex/ControllerResolver.php new file mode 100644 index 0000000..0a95e15 --- /dev/null +++ b/src/Silex/ControllerResolver.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex; + +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpKernel\Controller\ControllerResolver as BaseControllerResolver; +use Symfony\Component\HttpFoundation\Request; + +/** + * Adds Application as a valid argument for controllers. + * + * @author Fabien Potencier + * + * @deprecated This class can be dropped once Symfony 3.0 is not supported anymore. + */ +class ControllerResolver extends BaseControllerResolver +{ + protected $app; + + /** + * Constructor. + * + * @param Application $app An Application instance + * @param LoggerInterface $logger A LoggerInterface instance + */ + public function __construct(Application $app, LoggerInterface $logger = null) + { + $this->app = $app; + + parent::__construct($logger); + } + + protected function doGetArguments(Request $request, $controller, array $parameters) + { + foreach ($parameters as $param) { + if ($param->getClass() && $param->getClass()->isInstance($this->app)) { + $request->attributes->set($param->getName(), $this->app); + + break; + } + } + + return parent::doGetArguments($request, $controller, $parameters); + } +} diff --git a/src/Silex/EventListener/ConverterListener.php b/src/Silex/EventListener/ConverterListener.php new file mode 100644 index 0000000..2fa93c1 --- /dev/null +++ b/src/Silex/EventListener/ConverterListener.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\EventListener; + +use Silex\CallbackResolver; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Event\FilterControllerEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Routing\RouteCollection; + +/** + * Handles converters. + * + * @author Fabien Potencier + */ +class ConverterListener implements EventSubscriberInterface +{ + protected $routes; + protected $callbackResolver; + + /** + * Constructor. + * + * @param RouteCollection $routes A RouteCollection instance + * @param CallbackResolver $callbackResolver A CallbackResolver instance + */ + public function __construct(RouteCollection $routes, CallbackResolver $callbackResolver) + { + $this->routes = $routes; + $this->callbackResolver = $callbackResolver; + } + + /** + * Handles converters. + * + * @param FilterControllerEvent $event The event to handle + */ + public function onKernelController(FilterControllerEvent $event) + { + $request = $event->getRequest(); + $route = $this->routes->get($request->attributes->get('_route')); + if ($route && $converters = $route->getOption('_converters')) { + foreach ($converters as $name => $callback) { + $callback = $this->callbackResolver->resolveCallback($callback); + + $request->attributes->set($name, call_user_func($callback, $request->attributes->get($name), $request)); + } + } + } + + public static function getSubscribedEvents() + { + return array( + KernelEvents::CONTROLLER => 'onKernelController', + ); + } +} diff --git a/src/Silex/EventListener/LogListener.php b/src/Silex/EventListener/LogListener.php new file mode 100644 index 0000000..5f3cc90 --- /dev/null +++ b/src/Silex/EventListener/LogListener.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\EventListener; + +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\RedirectResponse; + +/** + * Logs request, response, and exceptions. + */ +class LogListener implements EventSubscriberInterface +{ + protected $logger; + protected $exceptionLogFilter; + + public function __construct(LoggerInterface $logger, $exceptionLogFilter = null) + { + $this->logger = $logger; + if (null === $exceptionLogFilter) { + $exceptionLogFilter = function (\Exception $e) { + if ($e instanceof HttpExceptionInterface && $e->getStatusCode() < 500) { + return LogLevel::ERROR; + } + + return LogLevel::CRITICAL; + }; + } + + $this->exceptionLogFilter = $exceptionLogFilter; + } + + /** + * Logs master requests on event KernelEvents::REQUEST. + * + * @param GetResponseEvent $event + */ + public function onKernelRequest(GetResponseEvent $event) + { + if (!$event->isMasterRequest()) { + return; + } + + $this->logRequest($event->getRequest()); + } + + /** + * Logs master response on event KernelEvents::RESPONSE. + * + * @param FilterResponseEvent $event + */ + public function onKernelResponse(FilterResponseEvent $event) + { + if (!$event->isMasterRequest()) { + return; + } + + $this->logResponse($event->getResponse()); + } + + /** + * Logs uncaught exceptions on event KernelEvents::EXCEPTION. + * + * @param GetResponseForExceptionEvent $event + */ + public function onKernelException(GetResponseForExceptionEvent $event) + { + $this->logException($event->getException()); + } + + /** + * Logs a request. + * + * @param Request $request + */ + protected function logRequest(Request $request) + { + $this->logger->log(LogLevel::DEBUG, '> '.$request->getMethod().' '.$request->getRequestUri()); + } + + /** + * Logs a response. + * + * @param Response $response + */ + protected function logResponse(Response $response) + { + $message = '< '.$response->getStatusCode(); + + if ($response instanceof RedirectResponse) { + $message .= ' '.$response->getTargetUrl(); + } + + $this->logger->log(LogLevel::DEBUG, $message); + } + + /** + * Logs an exception. + */ + protected function logException(\Exception $e) + { + $this->logger->log(call_user_func($this->exceptionLogFilter, $e), sprintf('%s: %s (uncaught exception) at %s line %s', get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()), array('exception' => $e)); + } + + public static function getSubscribedEvents() + { + return array( + KernelEvents::REQUEST => array('onKernelRequest', 0), + KernelEvents::RESPONSE => array('onKernelResponse', 0), + /* + * Priority -4 is used to come after those from SecurityServiceProvider (0) + * but before the error handlers added with Silex\Application::error (defaults to -8) + */ + KernelEvents::EXCEPTION => array('onKernelException', -4), + ); + } +} diff --git a/src/Silex/EventListener/MiddlewareListener.php b/src/Silex/EventListener/MiddlewareListener.php new file mode 100644 index 0000000..9b28ff1 --- /dev/null +++ b/src/Silex/EventListener/MiddlewareListener.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\EventListener; + +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Silex\Application; + +/** + * Manages the route middlewares. + * + * @author Fabien Potencier + */ +class MiddlewareListener implements EventSubscriberInterface +{ + protected $app; + + /** + * Constructor. + * + * @param Application $app An Application instance + */ + public function __construct(Application $app) + { + $this->app = $app; + } + + /** + * Runs before filters. + * + * @param GetResponseEvent $event The event to handle + */ + public function onKernelRequest(GetResponseEvent $event) + { + $request = $event->getRequest(); + $routeName = $request->attributes->get('_route'); + if (!$route = $this->app['routes']->get($routeName)) { + return; + } + + foreach ((array) $route->getOption('_before_middlewares') as $callback) { + $ret = call_user_func($this->app['callback_resolver']->resolveCallback($callback), $request, $this->app); + if ($ret instanceof Response) { + $event->setResponse($ret); + + return; + } elseif (null !== $ret) { + throw new \RuntimeException(sprintf('A before middleware for route "%s" returned an invalid response value. Must return null or an instance of Response.', $routeName)); + } + } + } + + /** + * Runs after filters. + * + * @param FilterResponseEvent $event The event to handle + */ + public function onKernelResponse(FilterResponseEvent $event) + { + $request = $event->getRequest(); + $routeName = $request->attributes->get('_route'); + if (!$route = $this->app['routes']->get($routeName)) { + return; + } + + foreach ((array) $route->getOption('_after_middlewares') as $callback) { + $response = call_user_func($this->app['callback_resolver']->resolveCallback($callback), $request, $event->getResponse(), $this->app); + if ($response instanceof Response) { + $event->setResponse($response); + } elseif (null !== $response) { + throw new \RuntimeException(sprintf('An after middleware for route "%s" returned an invalid response value. Must return null or an instance of Response.', $routeName)); + } + } + } + + public static function getSubscribedEvents() + { + return array( + // this must be executed after the late events defined with before() (and their priority is -512) + KernelEvents::REQUEST => array('onKernelRequest', -1024), + KernelEvents::RESPONSE => array('onKernelResponse', 128), + ); + } +} diff --git a/src/Silex/EventListener/StringToResponseListener.php b/src/Silex/EventListener/StringToResponseListener.php new file mode 100644 index 0000000..9fdba5f --- /dev/null +++ b/src/Silex/EventListener/StringToResponseListener.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\EventListener; + +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Response; + +/** + * Converts string responses to proper Response instances. + * + * @author Fabien Potencier + */ +class StringToResponseListener implements EventSubscriberInterface +{ + /** + * Handles string responses. + * + * @param GetResponseForControllerResultEvent $event The event to handle + */ + public function onKernelView(GetResponseForControllerResultEvent $event) + { + $response = $event->getControllerResult(); + + if (!( + null === $response + || is_array($response) + || $response instanceof Response + || (is_object($response) && !method_exists($response, '__toString')) + )) { + $event->setResponse(new Response((string) $response)); + } + } + + public static function getSubscribedEvents() + { + return array( + KernelEvents::VIEW => array('onKernelView', -10), + ); + } +} diff --git a/src/Silex/Exception/ControllerFrozenException.php b/src/Silex/Exception/ControllerFrozenException.php new file mode 100644 index 0000000..7f0d65f --- /dev/null +++ b/src/Silex/Exception/ControllerFrozenException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Exception; + +/** + * Exception, is thrown when a frozen controller is modified. + * + * @author Igor Wiedler + */ +class ControllerFrozenException extends \RuntimeException +{ +} diff --git a/src/Silex/ExceptionHandler.php b/src/Silex/ExceptionHandler.php new file mode 100644 index 0000000..34eb893 --- /dev/null +++ b/src/Silex/ExceptionHandler.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex; + +use Symfony\Component\Debug\ExceptionHandler as DebugExceptionHandler; +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; +use Symfony\Component\HttpKernel\KernelEvents; + +/** + * Default exception handler. + * + * @author Fabien Potencier + */ +class ExceptionHandler implements EventSubscriberInterface +{ + protected $debug; + + public function __construct($debug) + { + $this->debug = $debug; + } + + public function onSilexError(GetResponseForExceptionEvent $event) + { + $handler = new DebugExceptionHandler($this->debug); + + $exception = $event->getException(); + if (!$exception instanceof FlattenException) { + $exception = FlattenException::create($exception); + } + + $response = Response::create($handler->getHtml($exception), $exception->getStatusCode(), $exception->getHeaders())->setCharset(ini_get('default_charset')); + + $event->setResponse($response); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array(KernelEvents::EXCEPTION => array('onSilexError', -255)); + } +} diff --git a/src/Silex/ExceptionListenerWrapper.php b/src/Silex/ExceptionListenerWrapper.php new file mode 100644 index 0000000..e0d527b --- /dev/null +++ b/src/Silex/ExceptionListenerWrapper.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; +use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; + +/** + * Wraps exception listeners. + * + * @author Fabien Potencier + */ +class ExceptionListenerWrapper +{ + protected $app; + protected $callback; + + /** + * Constructor. + * + * @param Application $app An Application instance + * @param callable $callback + */ + public function __construct(Application $app, $callback) + { + $this->app = $app; + $this->callback = $callback; + } + + public function __invoke(GetResponseForExceptionEvent $event) + { + $exception = $event->getException(); + $this->callback = $this->app['callback_resolver']->resolveCallback($this->callback); + + if (!$this->shouldRun($exception)) { + return; + } + + $code = $exception instanceof HttpExceptionInterface ? $exception->getStatusCode() : 500; + + $response = call_user_func($this->callback, $exception, $event->getRequest(), $code); + + $this->ensureResponse($response, $event); + } + + protected function shouldRun(\Exception $exception) + { + if (is_array($this->callback)) { + $callbackReflection = new \ReflectionMethod($this->callback[0], $this->callback[1]); + } elseif (is_object($this->callback) && !$this->callback instanceof \Closure) { + $callbackReflection = new \ReflectionObject($this->callback); + $callbackReflection = $callbackReflection->getMethod('__invoke'); + } else { + $callbackReflection = new \ReflectionFunction($this->callback); + } + + if ($callbackReflection->getNumberOfParameters() > 0) { + $parameters = $callbackReflection->getParameters(); + $expectedException = $parameters[0]; + if ($expectedException->getClass() && !$expectedException->getClass()->isInstance($exception)) { + return false; + } + } + + return true; + } + + protected function ensureResponse($response, GetResponseForExceptionEvent $event) + { + if ($response instanceof Response) { + $event->setResponse($response); + } else { + $viewEvent = new GetResponseForControllerResultEvent($this->app['kernel'], $event->getRequest(), $event->getRequestType(), $response); + $this->app['dispatcher']->dispatch(KernelEvents::VIEW, $viewEvent); + + if ($viewEvent->hasResponse()) { + $event->setResponse($viewEvent->getResponse()); + } + } + } +} diff --git a/src/Silex/Provider/AssetServiceProvider.php b/src/Silex/Provider/AssetServiceProvider.php new file mode 100644 index 0000000..6793f67 --- /dev/null +++ b/src/Silex/Provider/AssetServiceProvider.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Symfony\Component\Asset\Packages; +use Symfony\Component\Asset\Package; +use Symfony\Component\Asset\PathPackage; +use Symfony\Component\Asset\UrlPackage; +use Symfony\Component\Asset\Context\RequestStackContext; +use Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy; +use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; + +/** + * Symfony Asset component Provider. + * + * @author Fabien Potencier + */ +class AssetServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['assets.packages'] = function ($app) { + $packages = array(); + foreach ($app['assets.named_packages'] as $name => $package) { + $version = $app['assets.strategy_factory'](isset($package['version']) ? $package['version'] : '', isset($package['version_format']) ? $package['version_format'] : null); + + $packages[$name] = $app['assets.package_factory'](isset($package['base_path']) ? $package['base_path'] : '', isset($package['base_urls']) ? $package['base_urls'] : array(), $version, $name); + } + + return new Packages($app['assets.default_package'], $packages); + }; + + $app['assets.default_package'] = function ($app) { + $version = $app['assets.strategy_factory']($app['assets.version'], $app['assets.version_format']); + + return $app['assets.package_factory']($app['assets.base_path'], $app['assets.base_urls'], $version, 'default'); + }; + + $app['assets.context'] = function ($app) { + return new RequestStackContext($app['request_stack']); + }; + + $app['assets.base_path'] = ''; + $app['assets.base_urls'] = array(); + $app['assets.version'] = null; + $app['assets.version_format'] = null; + + $app['assets.named_packages'] = array(); + + // prototypes + + $app['assets.strategy_factory'] = $app->protect(function ($version, $format) use ($app) { + if (!$version) { + return new EmptyVersionStrategy(); + } + + return new StaticVersionStrategy($version, $format); + }); + + $app['assets.package_factory'] = $app->protect(function ($basePath, $baseUrls, $version, $name) use ($app) { + if ($basePath && $baseUrls) { + throw new \LogicException(sprintf('Asset package "%s" cannot have base URLs and base paths.', $name)); + } + + if (!$baseUrls) { + return new PathPackage($basePath, $version, $app['assets.context']); + } + + return new UrlPackage($baseUrls, $version, $app['assets.context']); + }); + } +} diff --git a/src/Silex/Provider/CsrfServiceProvider.php b/src/Silex/Provider/CsrfServiceProvider.php new file mode 100644 index 0000000..eb6e882 --- /dev/null +++ b/src/Silex/Provider/CsrfServiceProvider.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Symfony\Component\Security\Csrf\CsrfTokenManager; +use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator; +use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage; +use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage; + +/** + * Symfony CSRF Security component Provider. + * + * @author Fabien Potencier + */ +class CsrfServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['csrf.token_manager'] = function ($app) { + return new CsrfTokenManager($app['csrf.token_generator'], $app['csrf.token_storage']); + }; + + $app['csrf.token_storage'] = function ($app) { + if (isset($app['session'])) { + return new SessionTokenStorage($app['session'], $app['csrf.session_namespace']); + } + + return new NativeSessionTokenStorage($app['csrf.session_namespace']); + }; + + $app['csrf.token_generator'] = function ($app) { + return new UriSafeTokenGenerator(); + }; + + $app['csrf.session_namespace'] = '_csrf'; + } +} diff --git a/src/Silex/Provider/DoctrineServiceProvider.php b/src/Silex/Provider/DoctrineServiceProvider.php new file mode 100644 index 0000000..9c71d5b --- /dev/null +++ b/src/Silex/Provider/DoctrineServiceProvider.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Configuration; +use Doctrine\Common\EventManager; +use Symfony\Bridge\Doctrine\Logger\DbalLogger; + +/** + * Doctrine DBAL Provider. + * + * @author Fabien Potencier + */ +class DoctrineServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['db.default_options'] = array( + 'driver' => 'pdo_mysql', + 'dbname' => null, + 'host' => 'localhost', + 'user' => 'root', + 'password' => null, + ); + + $app['dbs.options.initializer'] = $app->protect(function () use ($app) { + static $initialized = false; + + if ($initialized) { + return; + } + + $initialized = true; + + if (!isset($app['dbs.options'])) { + $app['dbs.options'] = array('default' => isset($app['db.options']) ? $app['db.options'] : array()); + } + + $tmp = $app['dbs.options']; + foreach ($tmp as $name => &$options) { + $options = array_replace($app['db.default_options'], $options); + + if (!isset($app['dbs.default'])) { + $app['dbs.default'] = $name; + } + } + $app['dbs.options'] = $tmp; + }); + + $app['dbs'] = function ($app) { + $app['dbs.options.initializer'](); + + $dbs = new Container(); + foreach ($app['dbs.options'] as $name => $options) { + if ($app['dbs.default'] === $name) { + // we use shortcuts here in case the default has been overridden + $config = $app['db.config']; + $manager = $app['db.event_manager']; + } else { + $config = $app['dbs.config'][$name]; + $manager = $app['dbs.event_manager'][$name]; + } + + $dbs[$name] = function ($dbs) use ($options, $config, $manager) { + return DriverManager::getConnection($options, $config, $manager); + }; + } + + return $dbs; + }; + + $app['dbs.config'] = function ($app) { + $app['dbs.options.initializer'](); + + $configs = new Container(); + $addLogger = isset($app['logger']) && null !== $app['logger'] && class_exists('Symfony\Bridge\Doctrine\Logger\DbalLogger'); + foreach ($app['dbs.options'] as $name => $options) { + $configs[$name] = new Configuration(); + if ($addLogger) { + $configs[$name]->setSQLLogger(new DbalLogger($app['logger'], isset($app['stopwatch']) ? $app['stopwatch'] : null)); + } + } + + return $configs; + }; + + $app['dbs.event_manager'] = function ($app) { + $app['dbs.options.initializer'](); + + $managers = new Container(); + foreach ($app['dbs.options'] as $name => $options) { + $managers[$name] = new EventManager(); + } + + return $managers; + }; + + // shortcuts for the "first" DB + $app['db'] = function ($app) { + $dbs = $app['dbs']; + + return $dbs[$app['dbs.default']]; + }; + + $app['db.config'] = function ($app) { + $dbs = $app['dbs.config']; + + return $dbs[$app['dbs.default']]; + }; + + $app['db.event_manager'] = function ($app) { + $dbs = $app['dbs.event_manager']; + + return $dbs[$app['dbs.default']]; + }; + } +} diff --git a/src/Silex/Provider/ExceptionHandlerServiceProvider.php b/src/Silex/Provider/ExceptionHandlerServiceProvider.php new file mode 100644 index 0000000..1c6f202 --- /dev/null +++ b/src/Silex/Provider/ExceptionHandlerServiceProvider.php @@ -0,0 +1,32 @@ +addSubscriber($app['exception_handler']); + } + } +} diff --git a/src/Silex/Provider/Form/SilexFormExtension.php b/src/Silex/Provider/Form/SilexFormExtension.php new file mode 100644 index 0000000..12efbdf --- /dev/null +++ b/src/Silex/Provider/Form/SilexFormExtension.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider\Form; + +use Pimple\Container; +use Symfony\Component\Form\Exception\InvalidArgumentException; +use Symfony\Component\Form\FormExtensionInterface; +use Symfony\Component\Form\FormTypeGuesserChain; + +class SilexFormExtension implements FormExtensionInterface +{ + private $app; + private $types; + private $typeExtensions; + private $guessers; + private $guesserLoaded = false; + private $guesser; + + public function __construct(Container $app, array $types, array $typeExtensions, array $guessers) + { + $this->app = $app; + $this->setTypes($types); + $this->setTypeExtensions($typeExtensions); + $this->setGuessers($guessers); + } + + public function getType($name) + { + if (!isset($this->types[$name])) { + throw new InvalidArgumentException(sprintf('The type "%s" is not the name of a registered form type.', $name)); + } + if (!is_object($this->types[$name])) { + $this->types[$name] = $this->app[$this->types[$name]]; + } + + return $this->types[$name]; + } + + public function hasType($name) + { + return isset($this->types[$name]); + } + + public function getTypeExtensions($name) + { + return isset($this->typeExtensions[$name]) ? $this->typeExtensions[$name] : []; + } + + public function hasTypeExtensions($name) + { + return isset($this->typeExtensions[$name]); + } + + public function getTypeGuesser() + { + if (!$this->guesserLoaded) { + $this->guesserLoaded = true; + + if ($this->guessers) { + $guessers = []; + foreach ($this->guessers as $guesser) { + if (!is_object($guesser)) { + $guesser = $this->app[$guesser]; + } + $guessers[] = $guesser; + } + $this->guesser = new FormTypeGuesserChain($guessers); + } + } + + return $this->guesser; + } + + private function setTypes(array $types) + { + $this->types = []; + foreach ($types as $type) { + if (!is_object($type)) { + if (!isset($this->app[$type])) { + throw new InvalidArgumentException(sprintf('Invalid form type. The silex service "%s" does not exist.', $type)); + } + $this->types[$type] = $type; + } else { + $this->types[get_class($type)] = $type; + } + } + } + + private function setTypeExtensions(array $typeExtensions) + { + $this->typeExtensions = []; + foreach ($typeExtensions as $extension) { + if (!is_object($extension)) { + if (!isset($this->app[$extension])) { + throw new InvalidArgumentException(sprintf('Invalid form type extension. The silex service "%s" does not exist.', $extension)); + } + $extension = $this->app[$extension]; + } + $this->typeExtensions[$extension->getExtendedType()][] = $extension; + } + } + + private function setGuessers(array $guessers) + { + $this->guessers = []; + foreach ($guessers as $guesser) { + if (!is_object($guesser) && !isset($this->app[$guesser])) { + throw new InvalidArgumentException(sprintf('Invalid form type guesser. The silex service "%s" does not exist.', $guesser)); + } + $this->guessers[] = $guesser; + } + } +} diff --git a/src/Silex/Provider/FormServiceProvider.php b/src/Silex/Provider/FormServiceProvider.php new file mode 100644 index 0000000..26ca42b --- /dev/null +++ b/src/Silex/Provider/FormServiceProvider.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Symfony\Component\Form\Extension\Csrf\CsrfExtension; +use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension; +use Symfony\Component\Form\Extension\Validator\ValidatorExtension as FormValidatorExtension; +use Symfony\Component\Form\Forms; +use Symfony\Component\Form\ResolvedFormTypeFactory; + +/** + * Symfony Form component Provider. + * + * @author Fabien Potencier + */ +class FormServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + if (!class_exists('Locale')) { + throw new \RuntimeException('You must either install the PHP intl extension or the Symfony Intl Component to use the Form extension.'); + } + + $app['form.types'] = function ($app) { + return array(); + }; + + $app['form.type.extensions'] = function ($app) { + return array(); + }; + + $app['form.type.guessers'] = function ($app) { + return array(); + }; + + $app['form.extension.csrf'] = function ($app) { + if (isset($app['translator'])) { + return new CsrfExtension($app['csrf.token_manager'], $app['translator']); + } + + return new CsrfExtension($app['csrf.token_manager']); + }; + + $app['form.extension.silex'] = function ($app) { + return new Form\SilexFormExtension($app, $app['form.types'], $app['form.type.extensions'], $app['form.type.guessers']); + }; + + $app['form.extensions'] = function ($app) { + $extensions = array( + new HttpFoundationExtension(), + ); + + if (isset($app['csrf.token_manager'])) { + $extensions[] = $app['form.extension.csrf']; + } + + if (isset($app['validator'])) { + $extensions[] = new FormValidatorExtension($app['validator']); + } + $extensions[] = $app['form.extension.silex']; + + return $extensions; + }; + + $app['form.factory'] = function ($app) { + return Forms::createFormFactoryBuilder() + ->addExtensions($app['form.extensions']) + ->setResolvedTypeFactory($app['form.resolved_type_factory']) + ->getFormFactory() + ; + }; + + $app['form.resolved_type_factory'] = function ($app) { + return new ResolvedFormTypeFactory(); + }; + } +} diff --git a/src/Silex/Provider/HttpCache/HttpCache.php b/src/Silex/Provider/HttpCache/HttpCache.php new file mode 100644 index 0000000..b0ebb5c --- /dev/null +++ b/src/Silex/Provider/HttpCache/HttpCache.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider\HttpCache; + +use Symfony\Component\HttpKernel\HttpCache\HttpCache as BaseHttpCache; +use Symfony\Component\HttpFoundation\Request; + +/** + * HTTP Cache extension to allow using the run() shortcut. + * + * @author Fabien Potencier + */ +class HttpCache extends BaseHttpCache +{ + /** + * Handles the Request and delivers the Response. + * + * @param Request $request The Request object + */ + public function run(Request $request = null) + { + if (null === $request) { + $request = Request::createFromGlobals(); + } + + $response = $this->handle($request); + $response->send(); + $this->terminate($request, $response); + } +} diff --git a/src/Silex/Provider/HttpCacheServiceProvider.php b/src/Silex/Provider/HttpCacheServiceProvider.php new file mode 100644 index 0000000..8b3f37e --- /dev/null +++ b/src/Silex/Provider/HttpCacheServiceProvider.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Silex\Provider\HttpCache\HttpCache; +use Silex\Api\EventListenerProviderInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpKernel\HttpCache\Esi; +use Symfony\Component\HttpKernel\HttpCache\Store; +use Symfony\Component\HttpKernel\EventListener\SurrogateListener; + +/** + * Symfony HttpKernel component Provider for HTTP cache. + * + * @author Fabien Potencier + */ +class HttpCacheServiceProvider implements ServiceProviderInterface, EventListenerProviderInterface +{ + public function register(Container $app) + { + $app['http_cache'] = function ($app) { + $app['http_cache.options'] = array_replace( + array( + 'debug' => $app['debug'], + ), $app['http_cache.options'] + ); + + return new HttpCache($app, $app['http_cache.store'], $app['http_cache.esi'], $app['http_cache.options']); + }; + + $app['http_cache.esi'] = function ($app) { + return new Esi(); + }; + + $app['http_cache.store'] = function ($app) { + return new Store($app['http_cache.cache_dir']); + }; + + $app['http_cache.esi_listener'] = function ($app) { + return new SurrogateListener($app['http_cache.esi']); + }; + + $app['http_cache.options'] = array(); + } + + public function subscribe(Container $app, EventDispatcherInterface $dispatcher) + { + $dispatcher->addSubscriber($app['http_cache.esi_listener']); + } +} diff --git a/src/Silex/Provider/HttpFragmentServiceProvider.php b/src/Silex/Provider/HttpFragmentServiceProvider.php new file mode 100644 index 0000000..9a641f4 --- /dev/null +++ b/src/Silex/Provider/HttpFragmentServiceProvider.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Silex\Api\EventListenerProviderInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpKernel\Fragment\FragmentHandler; +use Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer; +use Symfony\Component\HttpKernel\Fragment\EsiFragmentRenderer; +use Symfony\Component\HttpKernel\Fragment\HIncludeFragmentRenderer; +use Symfony\Component\HttpKernel\EventListener\FragmentListener; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\HttpKernel\UriSigner; + +/** + * HttpKernel Fragment integration for Silex. + * + * @author Fabien Potencier + */ +class HttpFragmentServiceProvider implements ServiceProviderInterface, EventListenerProviderInterface +{ + public function register(Container $app) + { + $app['fragment.handler'] = function ($app) { + return new FragmentHandler($app['request_stack'], $app['fragment.renderers'], $app['debug']); + }; + + $app['fragment.renderer.inline'] = function ($app) { + $renderer = new InlineFragmentRenderer($app['kernel'], $app['dispatcher']); + $renderer->setFragmentPath($app['fragment.path']); + + return $renderer; + }; + + $app['fragment.renderer.hinclude'] = function ($app) { + $renderer = new HIncludeFragmentRenderer(null, $app['uri_signer'], $app['fragment.renderer.hinclude.global_template'], $app['charset']); + $renderer->setFragmentPath($app['fragment.path']); + + return $renderer; + }; + + $app['fragment.renderer.esi'] = function ($app) { + $renderer = new EsiFragmentRenderer($app['http_cache.esi'], $app['fragment.renderer.inline']); + $renderer->setFragmentPath($app['fragment.path']); + + return $renderer; + }; + + $app['fragment.listener'] = function ($app) { + return new FragmentListener($app['uri_signer'], $app['fragment.path']); + }; + + $app['uri_signer'] = function ($app) { + return new UriSigner($app['uri_signer.secret']); + }; + + $app['uri_signer.secret'] = md5(__DIR__); + $app['fragment.path'] = '/_fragment'; + $app['fragment.renderer.hinclude.global_template'] = null; + $app['fragment.renderers'] = function ($app) { + $renderers = array($app['fragment.renderer.inline'], $app['fragment.renderer.hinclude']); + + if (isset($app['http_cache.esi'])) { + $renderers[] = $app['fragment.renderer.esi']; + } + + return $renderers; + }; + } + + public function subscribe(Container $app, EventDispatcherInterface $dispatcher) + { + $dispatcher->addSubscriber($app['fragment.listener']); + } +} diff --git a/src/Silex/Provider/HttpKernelServiceProvider.php b/src/Silex/Provider/HttpKernelServiceProvider.php new file mode 100644 index 0000000..226ed1d --- /dev/null +++ b/src/Silex/Provider/HttpKernelServiceProvider.php @@ -0,0 +1,95 @@ += 30100) { + return new SfControllerResolver($app['logger']); + } + + return new ControllerResolver($app, $app['logger']); + }; + + if (Kernel::VERSION_ID >= 30100) { + $app['argument_metadata_factory'] = function ($app) { + return new ArgumentMetadataFactory(); + }; + $app['argument_value_resolvers'] = function ($app) { + if (Kernel::VERSION_ID < 30200) { + return array( + new AppArgumentValueResolver($app), + new RequestAttributeValueResolver(), + new RequestValueResolver(), + new DefaultValueResolver(), + new VariadicValueResolver(), + ); + } + + return array_merge(array(new AppArgumentValueResolver($app)), ArgumentResolver::getDefaultArgumentValueResolvers()); + }; + } + + $app['argument_resolver'] = function ($app) { + if (Kernel::VERSION_ID >= 30100) { + return new ArgumentResolver($app['argument_metadata_factory'], $app['argument_value_resolvers']); + } + }; + + $app['kernel'] = function ($app) { + return new HttpKernel($app['dispatcher'], $app['resolver'], $app['request_stack'], $app['argument_resolver']); + }; + + $app['request_stack'] = function () { + return new RequestStack(); + }; + + $app['dispatcher'] = function () { + return new EventDispatcher(); + }; + + $app['callback_resolver'] = function ($app) { + return new CallbackResolver($app); + }; + } + + /** + * {@inheritdoc} + */ + public function subscribe(Container $app, EventDispatcherInterface $dispatcher) + { + $dispatcher->addSubscriber(new ResponseListener($app['charset'])); + $dispatcher->addSubscriber(new MiddlewareListener($app)); + $dispatcher->addSubscriber(new ConverterListener($app['routes'], $app['callback_resolver'])); + $dispatcher->addSubscriber(new StringToResponseListener()); + } +} diff --git a/src/Silex/Provider/LICENSE b/src/Silex/Provider/LICENSE new file mode 100644 index 0000000..bc6ad04 --- /dev/null +++ b/src/Silex/Provider/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010-2015 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Silex/Provider/Locale/LocaleListener.php b/src/Silex/Provider/Locale/LocaleListener.php new file mode 100644 index 0000000..d500264 --- /dev/null +++ b/src/Silex/Provider/Locale/LocaleListener.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider\Locale; + +use Pimple\Container; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Event\FinishRequestEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Initializes the locale based on the current request. + * + * @author Fabien Potencier + * @author Jérôme Tamarelle + */ +class LocaleListener implements EventSubscriberInterface +{ + private $app; + private $defaultLocale; + private $requestStack; + private $requestContext; + + public function __construct(Container $app, $defaultLocale = 'en', RequestStack $requestStack, RequestContext $requestContext = null) + { + $this->app = $app; + $this->defaultLocale = $defaultLocale; + $this->requestStack = $requestStack; + $this->requestContext = $requestContext; + } + + public function onKernelRequest(GetResponseEvent $event) + { + $request = $event->getRequest(); + $request->setDefaultLocale($this->defaultLocale); + + $this->setLocale($request); + $this->setRouterContext($request); + + $this->app['locale'] = $request->getLocale(); + } + + public function onKernelFinishRequest(FinishRequestEvent $event) + { + if (null !== $parentRequest = $this->requestStack->getParentRequest()) { + $this->setRouterContext($parentRequest); + } + } + + private function setLocale(Request $request) + { + if ($locale = $request->attributes->get('_locale')) { + $request->setLocale($locale); + } + } + + private function setRouterContext(Request $request) + { + if (null !== $this->requestContext) { + $this->requestContext->setParameter('_locale', $request->getLocale()); + } + } + + public static function getSubscribedEvents() + { + return array( + // must be registered after the Router to have access to the _locale + KernelEvents::REQUEST => array(array('onKernelRequest', 16)), + KernelEvents::FINISH_REQUEST => array(array('onKernelFinishRequest', 0)), + ); + } +} diff --git a/src/Silex/Provider/LocaleServiceProvider.php b/src/Silex/Provider/LocaleServiceProvider.php new file mode 100644 index 0000000..ddea81b --- /dev/null +++ b/src/Silex/Provider/LocaleServiceProvider.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Silex\Api\EventListenerProviderInterface; +use Silex\Provider\Locale\LocaleListener; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; + +/** + * Locale Provider. + * + * @author Fabien Potencier + */ +class LocaleServiceProvider implements ServiceProviderInterface, EventListenerProviderInterface +{ + public function register(Container $app) + { + $app['locale.listener'] = function ($app) { + return new LocaleListener($app, $app['locale'], $app['request_stack'], isset($app['request_context']) ? $app['request_context'] : null); + }; + + $app['locale'] = 'en'; + } + + public function subscribe(Container $app, EventDispatcherInterface $dispatcher) + { + $dispatcher->addSubscriber($app['locale.listener']); + } +} diff --git a/src/Silex/Provider/MonologServiceProvider.php b/src/Silex/Provider/MonologServiceProvider.php new file mode 100644 index 0000000..34654e9 --- /dev/null +++ b/src/Silex/Provider/MonologServiceProvider.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Monolog\Formatter\LineFormatter; +use Monolog\Logger; +use Monolog\Handler; +use Monolog\ErrorHandler; +use Silex\Application; +use Silex\Api\BootableProviderInterface; +use Symfony\Bridge\Monolog\Handler\DebugHandler; +use Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy; +use Silex\EventListener\LogListener; + +/** + * Monolog Provider. + * + * @author Fabien Potencier + */ +class MonologServiceProvider implements ServiceProviderInterface, BootableProviderInterface +{ + public function register(Container $app) + { + $app['logger'] = function () use ($app) { + return $app['monolog']; + }; + + if ($bridge = class_exists('Symfony\Bridge\Monolog\Logger')) { + $app['monolog.handler.debug'] = function () use ($app) { + $level = MonologServiceProvider::translateLevel($app['monolog.level']); + + return new DebugHandler($level); + }; + + if (isset($app['request_stack'])) { + $app['monolog.not_found_activation_strategy'] = function () use ($app) { + return new NotFoundActivationStrategy($app['request_stack'], array('^/'), $app['monolog.level']); + }; + } + } + + $app['monolog.logger.class'] = $bridge ? 'Symfony\Bridge\Monolog\Logger' : 'Monolog\Logger'; + + $app['monolog'] = function ($app) { + $log = new $app['monolog.logger.class']($app['monolog.name']); + + $handler = new Handler\GroupHandler($app['monolog.handlers']); + if (isset($app['monolog.not_found_activation_strategy'])) { + $handler = new Handler\FingersCrossedHandler($handler, $app['monolog.not_found_activation_strategy']); + } + + $log->pushHandler($handler); + + if ($app['debug'] && isset($app['monolog.handler.debug'])) { + $log->pushHandler($app['monolog.handler.debug']); + } + + return $log; + }; + + $app['monolog.formatter'] = function () { + return new LineFormatter(); + }; + + $app['monolog.handler'] = $defaultHandler = function () use ($app) { + $level = MonologServiceProvider::translateLevel($app['monolog.level']); + + $handler = new Handler\StreamHandler($app['monolog.logfile'], $level, $app['monolog.bubble'], $app['monolog.permission']); + $handler->setFormatter($app['monolog.formatter']); + + return $handler; + }; + + $app['monolog.handlers'] = function () use ($app, $defaultHandler) { + $handlers = array(); + + // enables the default handler if a logfile was set or the monolog.handler service was redefined + if ($app['monolog.logfile'] || $defaultHandler !== $app->raw('monolog.handler')) { + $handlers[] = $app['monolog.handler']; + } + + return $handlers; + }; + + $app['monolog.level'] = function () { + return Logger::DEBUG; + }; + + $app['monolog.listener'] = function () use ($app) { + return new LogListener($app['logger'], $app['monolog.exception.logger_filter']); + }; + + $app['monolog.name'] = 'app'; + $app['monolog.bubble'] = true; + $app['monolog.permission'] = null; + $app['monolog.exception.logger_filter'] = null; + $app['monolog.logfile'] = null; + $app['monolog.use_error_handler'] = !$app['debug']; + } + + public function boot(Application $app) + { + if ($app['monolog.use_error_handler']) { + ErrorHandler::register($app['monolog']); + } + + if (isset($app['monolog.listener'])) { + $app['dispatcher']->addSubscriber($app['monolog.listener']); + } + } + + public static function translateLevel($name) + { + // level is already translated to logger constant, return as-is + if (is_int($name)) { + return $name; + } + + $levels = Logger::getLevels(); + $upper = strtoupper($name); + + if (!isset($levels[$upper])) { + throw new \InvalidArgumentException("Provided logging level '$name' does not exist. Must be a valid monolog logging level."); + } + + return $levels[$upper]; + } +} diff --git a/src/Silex/Provider/RememberMeServiceProvider.php b/src/Silex/Provider/RememberMeServiceProvider.php new file mode 100644 index 0000000..766631c --- /dev/null +++ b/src/Silex/Provider/RememberMeServiceProvider.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Silex\Api\EventListenerProviderInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Security\Core\Authentication\Provider\RememberMeAuthenticationProvider; +use Symfony\Component\Security\Http\Firewall\RememberMeListener; +use Symfony\Component\Security\Http\RememberMe\TokenBasedRememberMeServices; +use Symfony\Component\Security\Http\RememberMe\ResponseListener; + +/** + * Remember-me authentication for the SecurityServiceProvider. + * + * @author Jérôme Tamarelle + */ +class RememberMeServiceProvider implements ServiceProviderInterface, EventListenerProviderInterface +{ + public function register(Container $app) + { + $app['security.remember_me.response_listener'] = function ($app) { + if (!isset($app['security.token_storage'])) { + throw new \LogicException('You must register the SecurityServiceProvider to use the RememberMeServiceProvider'); + } + + return new ResponseListener(); + }; + + $app['security.authentication_listener.factory.remember_me'] = $app->protect(function ($name, $options) use ($app) { + if (empty($options['key'])) { + $options['key'] = $name; + } + + if (!isset($app['security.remember_me.service.'.$name])) { + $app['security.remember_me.service.'.$name] = $app['security.remember_me.service._proto']($name, $options); + } + + if (!isset($app['security.authentication_listener.'.$name.'.remember_me'])) { + $app['security.authentication_listener.'.$name.'.remember_me'] = $app['security.authentication_listener.remember_me._proto']($name, $options); + } + + if (!isset($app['security.authentication_provider.'.$name.'.remember_me'])) { + $app['security.authentication_provider.'.$name.'.remember_me'] = $app['security.authentication_provider.remember_me._proto']($name, $options); + } + + return array( + 'security.authentication_provider.'.$name.'.remember_me', + 'security.authentication_listener.'.$name.'.remember_me', + null, // entry point + 'remember_me', + ); + }); + + $app['security.remember_me.service._proto'] = $app->protect(function ($providerKey, $options) use ($app) { + return function () use ($providerKey, $options, $app) { + $options = array_replace(array( + 'name' => 'REMEMBERME', + 'lifetime' => 31536000, + 'path' => '/', + 'domain' => null, + 'secure' => false, + 'httponly' => true, + 'always_remember_me' => false, + 'remember_me_parameter' => '_remember_me', + ), $options); + + return new TokenBasedRememberMeServices(array($app['security.user_provider.'.$providerKey]), $options['key'], $providerKey, $options, $app['logger']); + }; + }); + + $app['security.authentication_listener.remember_me._proto'] = $app->protect(function ($providerKey) use ($app) { + return function () use ($app, $providerKey) { + $listener = new RememberMeListener( + $app['security.token_storage'], + $app['security.remember_me.service.'.$providerKey], + $app['security.authentication_manager'], + $app['logger'], + $app['dispatcher'] + ); + + return $listener; + }; + }); + + $app['security.authentication_provider.remember_me._proto'] = $app->protect(function ($name, $options) use ($app) { + return function () use ($app, $name, $options) { + return new RememberMeAuthenticationProvider($app['security.user_checker'], $options['key'], $name); + }; + }); + } + + public function subscribe(Container $app, EventDispatcherInterface $dispatcher) + { + $dispatcher->addSubscriber($app['security.remember_me.response_listener']); + } +} diff --git a/src/Silex/Provider/Routing/LazyRequestMatcher.php b/src/Silex/Provider/Routing/LazyRequestMatcher.php new file mode 100644 index 0000000..6837c79 --- /dev/null +++ b/src/Silex/Provider/Routing/LazyRequestMatcher.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider\Routing; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Matcher\RequestMatcherInterface; +use Symfony\Component\Routing\Matcher\UrlMatcherInterface; + +/** + * Implements a lazy UrlMatcher. + * + * @author Igor Wiedler + * @author Jérôme Tamarelle + */ +class LazyRequestMatcher implements RequestMatcherInterface +{ + private $factory; + + public function __construct(\Closure $factory) + { + $this->factory = $factory; + } + + /** + * Returns the corresponding RequestMatcherInterface instance. + * + * @return UrlMatcherInterface + */ + public function getRequestMatcher() + { + $matcher = call_user_func($this->factory); + if (!$matcher instanceof RequestMatcherInterface) { + throw new \LogicException("Factory supplied to LazyRequestMatcher must return implementation of Symfony\Component\Routing\RequestMatcherInterface."); + } + + return $matcher; + } + + /** + * {@inheritdoc} + */ + public function matchRequest(Request $request) + { + return $this->getRequestMatcher()->matchRequest($request); + } +} diff --git a/src/Silex/Provider/Routing/RedirectableUrlMatcher.php b/src/Silex/Provider/Routing/RedirectableUrlMatcher.php new file mode 100644 index 0000000..021328b --- /dev/null +++ b/src/Silex/Provider/Routing/RedirectableUrlMatcher.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider\Routing; + +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\Routing\Matcher\RedirectableUrlMatcher as BaseRedirectableUrlMatcher; + +/** + * Implements the RedirectableUrlMatcherInterface for Silex. + * + * @author Fabien Potencier + */ +class RedirectableUrlMatcher extends BaseRedirectableUrlMatcher +{ + /** + * {@inheritdoc} + */ + public function redirect($path, $route, $scheme = null) + { + $url = $this->context->getBaseUrl().$path; + $query = $this->context->getQueryString() ?: ''; + + if ($query !== '') { + $url .= '?'.$query; + } + + if ($this->context->getHost()) { + if ($scheme) { + $port = ''; + if ('http' === $scheme && 80 != $this->context->getHttpPort()) { + $port = ':'.$this->context->getHttpPort(); + } elseif ('https' === $scheme && 443 != $this->context->getHttpsPort()) { + $port = ':'.$this->context->getHttpsPort(); + } + + $url = $scheme.'://'.$this->context->getHost().$port.$url; + } + } + + return array( + '_controller' => function ($url) { return new RedirectResponse($url, 301); }, + '_route' => null, + 'url' => $url, + ); + } +} diff --git a/src/Silex/Provider/RoutingServiceProvider.php b/src/Silex/Provider/RoutingServiceProvider.php new file mode 100644 index 0000000..d040ba0 --- /dev/null +++ b/src/Silex/Provider/RoutingServiceProvider.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Silex\ControllerCollection; +use Silex\Api\EventListenerProviderInterface; +use Silex\Provider\Routing\RedirectableUrlMatcher; +use Silex\Provider\Routing\LazyRequestMatcher; +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\Generator\UrlGenerator; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\HttpKernel\EventListener\RouterListener; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; + +/** + * Symfony Routing component Provider. + * + * @author Fabien Potencier + */ +class RoutingServiceProvider implements ServiceProviderInterface, EventListenerProviderInterface +{ + public function register(Container $app) + { + $app['route_class'] = 'Silex\\Route'; + + $app['route_factory'] = $app->factory(function ($app) { + return new $app['route_class'](); + }); + + $app['routes_factory'] = $app->factory(function () { + return new RouteCollection(); + }); + + $app['routes'] = function ($app) { + return $app['routes_factory']; + }; + $app['url_generator'] = function ($app) { + return new UrlGenerator($app['routes'], $app['request_context']); + }; + + $app['request_matcher'] = function ($app) { + return new RedirectableUrlMatcher($app['routes'], $app['request_context']); + }; + + $app['request_context'] = function ($app) { + $context = new RequestContext(); + + $context->setHttpPort(isset($app['request.http_port']) ? $app['request.http_port'] : 80); + $context->setHttpsPort(isset($app['request.https_port']) ? $app['request.https_port'] : 443); + + return $context; + }; + + $app['controllers'] = function ($app) { + return $app['controllers_factory']; + }; + + $controllers_factory = function () use ($app, &$controllers_factory) { + return new ControllerCollection($app['route_factory'], $app['routes_factory'], $controllers_factory); + }; + $app['controllers_factory'] = $app->factory($controllers_factory); + + $app['routing.listener'] = function ($app) { + $urlMatcher = new LazyRequestMatcher(function () use ($app) { + return $app['request_matcher']; + }); + + return new RouterListener($urlMatcher, $app['request_stack'], $app['request_context'], $app['logger']); + }; + } + + public function subscribe(Container $app, EventDispatcherInterface $dispatcher) + { + $dispatcher->addSubscriber($app['routing.listener']); + } +} diff --git a/src/Silex/Provider/SecurityServiceProvider.php b/src/Silex/Provider/SecurityServiceProvider.php new file mode 100644 index 0000000..ee7d34b --- /dev/null +++ b/src/Silex/Provider/SecurityServiceProvider.php @@ -0,0 +1,684 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Silex\Application; +use Silex\Api\BootableProviderInterface; +use Silex\Api\ControllerProviderInterface; +use Silex\Api\EventListenerProviderInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\RequestMatcher; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\UserChecker; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; +use Symfony\Component\Security\Core\Encoder\EncoderFactory; +use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder; +use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder; +use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder; +use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider; +use Symfony\Component\Security\Core\Authentication\Provider\AnonymousAuthenticationProvider; +use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager; +use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver; +use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler; +use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; +use Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter; +use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; +use Symfony\Component\Security\Core\Role\RoleHierarchy; +use Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator; +use Symfony\Component\Security\Http\Firewall; +use Symfony\Component\Security\Http\FirewallMap; +use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener; +use Symfony\Component\Security\Http\Firewall\AccessListener; +use Symfony\Component\Security\Http\Firewall\BasicAuthenticationListener; +use Symfony\Component\Security\Http\Firewall\LogoutListener; +use Symfony\Component\Security\Http\Firewall\SwitchUserListener; +use Symfony\Component\Security\Http\Firewall\AnonymousAuthenticationListener; +use Symfony\Component\Security\Http\Firewall\ContextListener; +use Symfony\Component\Security\Http\Firewall\ExceptionListener; +use Symfony\Component\Security\Http\Firewall\ChannelListener; +use Symfony\Component\Security\Http\EntryPoint\FormAuthenticationEntryPoint; +use Symfony\Component\Security\Http\EntryPoint\BasicAuthenticationEntryPoint; +use Symfony\Component\Security\Http\EntryPoint\RetryAuthenticationEntryPoint; +use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy; +use Symfony\Component\Security\Http\Logout\SessionLogoutHandler; +use Symfony\Component\Security\Http\Logout\DefaultLogoutSuccessHandler; +use Symfony\Component\Security\Http\AccessMap; +use Symfony\Component\Security\Http\HttpUtils; +use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; +use Symfony\Component\Security\Guard\Firewall\GuardAuthenticationListener; +use Symfony\Component\Security\Guard\Provider\GuardAuthenticationProvider; + +/** + * Symfony Security component Provider. + * + * @author Fabien Potencier + */ +class SecurityServiceProvider implements ServiceProviderInterface, EventListenerProviderInterface, ControllerProviderInterface, BootableProviderInterface +{ + protected $fakeRoutes; + + public function register(Container $app) + { + // used to register routes for login_check and logout + $this->fakeRoutes = array(); + + $that = $this; + + $app['security.role_hierarchy'] = array(); + $app['security.access_rules'] = array(); + $app['security.hide_user_not_found'] = true; + $app['security.encoder.bcrypt.cost'] = 13; + + $app['security.authorization_checker'] = function ($app) { + return new AuthorizationChecker($app['security.token_storage'], $app['security.authentication_manager'], $app['security.access_manager']); + }; + + $app['security.token_storage'] = function ($app) { + return new TokenStorage(); + }; + + $app['user'] = $app->factory(function ($app) { + if (null === $token = $app['security.token_storage']->getToken()) { + return; + } + + if (!is_object($user = $token->getUser())) { + return; + } + + return $user; + }); + + $app['security.authentication_manager'] = function ($app) { + $manager = new AuthenticationProviderManager($app['security.authentication_providers']); + $manager->setEventDispatcher($app['dispatcher']); + + return $manager; + }; + + // by default, all users use the digest encoder + $app['security.encoder_factory'] = function ($app) { + return new EncoderFactory(array( + 'Symfony\Component\Security\Core\User\UserInterface' => $app['security.default_encoder'], + )); + }; + + // by default, all users use the BCrypt encoder + $app['security.default_encoder'] = function ($app) { + return $app['security.encoder.bcrypt']; + }; + + $app['security.encoder.digest'] = function ($app) { + return new MessageDigestPasswordEncoder(); + }; + + $app['security.encoder.bcrypt'] = function ($app) { + return new BCryptPasswordEncoder($app['security.encoder.bcrypt.cost']); + }; + + $app['security.encoder.pbkdf2'] = function ($app) { + return new Pbkdf2PasswordEncoder(); + }; + + $app['security.user_checker'] = function ($app) { + return new UserChecker(); + }; + + $app['security.access_manager'] = function ($app) { + return new AccessDecisionManager($app['security.voters']); + }; + + $app['security.voters'] = function ($app) { + return array( + new RoleHierarchyVoter(new RoleHierarchy($app['security.role_hierarchy'])), + new AuthenticatedVoter($app['security.trust_resolver']), + ); + }; + + $app['security.firewall'] = function ($app) { + return new Firewall($app['security.firewall_map'], $app['dispatcher']); + }; + + $app['security.channel_listener'] = function ($app) { + return new ChannelListener( + $app['security.access_map'], + new RetryAuthenticationEntryPoint( + isset($app['request.http_port']) ? $app['request.http_port'] : 80, + isset($app['request.https_port']) ? $app['request.https_port'] : 443 + ), + $app['logger'] + ); + }; + + // generate the build-in authentication factories + foreach (array('logout', 'pre_auth', 'guard', 'form', 'http', 'remember_me', 'anonymous') as $type) { + $entryPoint = null; + if ('http' === $type) { + $entryPoint = 'http'; + } elseif ('form' === $type) { + $entryPoint = 'form'; + } elseif ('guard' === $type) { + $entryPoint = 'guard'; + } + + $app['security.authentication_listener.factory.'.$type] = $app->protect(function ($name, $options) use ($type, $app, $entryPoint) { + if ($entryPoint && !isset($app['security.entry_point.'.$name.'.'.$entryPoint])) { + $app['security.entry_point.'.$name.'.'.$entryPoint] = $app['security.entry_point.'.$entryPoint.'._proto']($name, $options); + } + + if (!isset($app['security.authentication_listener.'.$name.'.'.$type])) { + $app['security.authentication_listener.'.$name.'.'.$type] = $app['security.authentication_listener.'.$type.'._proto']($name, $options); + } + + $provider = 'dao'; + if ('anonymous' === $type) { + $provider = 'anonymous'; + } elseif ('guard' === $type) { + $provider = 'guard'; + } + if (!isset($app['security.authentication_provider.'.$name.'.'.$provider])) { + $app['security.authentication_provider.'.$name.'.'.$provider] = $app['security.authentication_provider.'.$provider.'._proto']($name, $options); + } + + return array( + 'security.authentication_provider.'.$name.'.'.$provider, + 'security.authentication_listener.'.$name.'.'.$type, + $entryPoint ? 'security.entry_point.'.$name.'.'.$entryPoint : null, + $type, + ); + }); + } + + $app['security.firewall_map'] = function ($app) { + $positions = array('logout', 'pre_auth', 'guard', 'form', 'http', 'remember_me', 'anonymous'); + $providers = array(); + $configs = array(); + foreach ($app['security.firewalls'] as $name => $firewall) { + $entryPoint = null; + $pattern = isset($firewall['pattern']) ? $firewall['pattern'] : null; + $users = isset($firewall['users']) ? $firewall['users'] : array(); + $security = isset($firewall['security']) ? (bool) $firewall['security'] : true; + $stateless = isset($firewall['stateless']) ? (bool) $firewall['stateless'] : false; + $context = isset($firewall['context']) ? $firewall['context'] : $name; + unset($firewall['pattern'], $firewall['users'], $firewall['security'], $firewall['stateless'], $firewall['context']); + + $protected = false === $security ? false : count($firewall); + + $listeners = array('security.channel_listener'); + + if ($protected) { + if (!isset($app['security.context_listener.'.$name])) { + if (!isset($app['security.user_provider.'.$name])) { + $app['security.user_provider.'.$name] = is_array($users) ? $app['security.user_provider.inmemory._proto']($users) : $users; + } + + $app['security.context_listener.'.$name] = $app['security.context_listener._proto']($name, array($app['security.user_provider.'.$name])); + } + + if (false === $stateless) { + $listeners[] = 'security.context_listener.'.$context; + } + + $factories = array(); + foreach ($positions as $position) { + $factories[$position] = array(); + } + + foreach ($firewall as $type => $options) { + if ('switch_user' === $type) { + continue; + } + + // normalize options + if (!is_array($options)) { + if (!$options) { + continue; + } + + $options = array(); + } + + if (!isset($app['security.authentication_listener.factory.'.$type])) { + throw new \LogicException(sprintf('The "%s" authentication entry is not registered.', $type)); + } + + $options['stateless'] = $stateless; + + list($providerId, $listenerId, $entryPointId, $position) = $app['security.authentication_listener.factory.'.$type]($name, $options); + + if (null !== $entryPointId) { + $entryPoint = $entryPointId; + } + + $factories[$position][] = $listenerId; + $providers[] = $providerId; + } + + foreach ($positions as $position) { + foreach ($factories[$position] as $listener) { + $listeners[] = $listener; + } + } + + $listeners[] = 'security.access_listener'; + + if (isset($firewall['switch_user'])) { + $app['security.switch_user.'.$name] = $app['security.authentication_listener.switch_user._proto']($name, $firewall['switch_user']); + + $listeners[] = 'security.switch_user.'.$name; + } + + if (!isset($app['security.exception_listener.'.$name])) { + if (null == $entryPoint) { + $app[$entryPoint = 'security.entry_point.'.$name.'.form'] = $app['security.entry_point.form._proto']($name, array()); + } + $accessDeniedHandler = null; + if (isset($app['security.access_denied_handler.'.$name])) { + $accessDeniedHandler = $app['security.access_denied_handler.'.$name]; + } + $app['security.exception_listener.'.$name] = $app['security.exception_listener._proto']($entryPoint, $name, $accessDeniedHandler); + } + } + + $configs[$name] = array($pattern, $listeners, $protected); + } + + $app['security.authentication_providers'] = array_map(function ($provider) use ($app) { + return $app[$provider]; + }, array_unique($providers)); + + $map = new FirewallMap(); + foreach ($configs as $name => $config) { + $map->add( + is_string($config[0]) ? new RequestMatcher($config[0]) : $config[0], + array_map(function ($listenerId) use ($app, $name) { + $listener = $app[$listenerId]; + + if (isset($app['security.remember_me.service.'.$name])) { + if ($listener instanceof AbstractAuthenticationListener || $listener instanceof GuardAuthenticationListener) { + $listener->setRememberMeServices($app['security.remember_me.service.'.$name]); + } + if ($listener instanceof LogoutListener) { + $listener->addHandler($app['security.remember_me.service.'.$name]); + } + } + + return $listener; + }, $config[1]), + $config[2] ? $app['security.exception_listener.'.$name] : null + ); + } + + return $map; + }; + + $app['security.access_listener'] = function ($app) { + return new AccessListener( + $app['security.token_storage'], + $app['security.access_manager'], + $app['security.access_map'], + $app['security.authentication_manager'], + $app['logger'] + ); + }; + + $app['security.access_map'] = function ($app) { + $map = new AccessMap(); + + foreach ($app['security.access_rules'] as $rule) { + if (is_string($rule[0])) { + $rule[0] = new RequestMatcher($rule[0]); + } elseif (is_array($rule[0])) { + $rule[0] += [ + 'path' => null, + 'host' => null, + 'methods' => null, + 'ips' => null, + 'attributes' => array(), + 'schemes' => null, + ]; + $rule[0] = new RequestMatcher($rule[0]['path'], $rule[0]['host'], $rule[0]['methods'], $rule[0]['ips'], $rule[0]['attributes'], $rule[0]['schemes']); + } + $map->add($rule[0], (array) $rule[1], isset($rule[2]) ? $rule[2] : null); + } + + return $map; + }; + + $app['security.trust_resolver'] = function ($app) { + return new AuthenticationTrustResolver('Symfony\Component\Security\Core\Authentication\Token\AnonymousToken', 'Symfony\Component\Security\Core\Authentication\Token\RememberMeToken'); + }; + + $app['security.session_strategy'] = function ($app) { + return new SessionAuthenticationStrategy(SessionAuthenticationStrategy::MIGRATE); + }; + + $app['security.http_utils'] = function ($app) { + return new HttpUtils($app['url_generator'], $app['request_matcher']); + }; + + $app['security.last_error'] = $app->protect(function (Request $request) { + if ($request->attributes->has(Security::AUTHENTICATION_ERROR)) { + return $request->attributes->get(Security::AUTHENTICATION_ERROR)->getMessage(); + } + + $session = $request->getSession(); + if ($session && $session->has(Security::AUTHENTICATION_ERROR)) { + $message = $session->get(Security::AUTHENTICATION_ERROR)->getMessage(); + $session->remove(Security::AUTHENTICATION_ERROR); + + return $message; + } + }); + + // prototypes (used by the Firewall Map) + + $app['security.context_listener._proto'] = $app->protect(function ($providerKey, $userProviders) use ($app) { + return function () use ($app, $userProviders, $providerKey) { + return new ContextListener( + $app['security.token_storage'], + $userProviders, + $providerKey, + $app['logger'], + $app['dispatcher'] + ); + }; + }); + + $app['security.user_provider.inmemory._proto'] = $app->protect(function ($params) use ($app) { + return function () use ($app, $params) { + $users = array(); + foreach ($params as $name => $user) { + $users[$name] = array('roles' => (array) $user[0], 'password' => $user[1]); + } + + return new InMemoryUserProvider($users); + }; + }); + + $app['security.exception_listener._proto'] = $app->protect(function ($entryPoint, $name, $accessDeniedHandler = null) use ($app) { + return function () use ($app, $entryPoint, $name, $accessDeniedHandler) { + return new ExceptionListener( + $app['security.token_storage'], + $app['security.trust_resolver'], + $app['security.http_utils'], + $name, + $app[$entryPoint], + null, // errorPage + $accessDeniedHandler, + $app['logger'] + ); + }; + }); + + $app['security.authentication.success_handler._proto'] = $app->protect(function ($name, $options) use ($app) { + return function () use ($name, $options, $app) { + $handler = new DefaultAuthenticationSuccessHandler( + $app['security.http_utils'], + $options + ); + $handler->setProviderKey($name); + + return $handler; + }; + }); + + $app['security.authentication.failure_handler._proto'] = $app->protect(function ($name, $options) use ($app) { + return function () use ($name, $options, $app) { + return new DefaultAuthenticationFailureHandler( + $app, + $app['security.http_utils'], + $options, + $app['logger'] + ); + }; + }); + + $app['security.authentication_listener.guard._proto'] = $app->protect(function ($providerKey, $options) use ($app, $that) { + return function () use ($app, $providerKey, $options, $that) { + if (!isset($app['security.authentication.guard_handler'])) { + $app['security.authentication.guard_handler'] = new GuardAuthenticatorHandler($app['security.token_storage'], $app['dispatcher']); + } + + $authenticators = array(); + foreach ($options['authenticators'] as $authenticatorId) { + $authenticators[] = $app[$authenticatorId]; + } + + return new GuardAuthenticationListener( + $app['security.authentication.guard_handler'], + $app['security.authentication_manager'], + $providerKey, + $authenticators, + $app['logger'] + ); + }; + }); + + $app['security.authentication_listener.form._proto'] = $app->protect(function ($name, $options) use ($app, $that) { + return function () use ($app, $name, $options, $that) { + $that->addFakeRoute( + 'match', + $tmp = isset($options['check_path']) ? $options['check_path'] : '/login_check', + str_replace('/', '_', ltrim($tmp, '/')) + ); + + $class = isset($options['listener_class']) ? $options['listener_class'] : 'Symfony\\Component\\Security\\Http\\Firewall\\UsernamePasswordFormAuthenticationListener'; + + if (!isset($app['security.authentication.success_handler.'.$name])) { + $app['security.authentication.success_handler.'.$name] = $app['security.authentication.success_handler._proto']($name, $options); + } + + if (!isset($app['security.authentication.failure_handler.'.$name])) { + $app['security.authentication.failure_handler.'.$name] = $app['security.authentication.failure_handler._proto']($name, $options); + } + + return new $class( + $app['security.token_storage'], + $app['security.authentication_manager'], + isset($app['security.session_strategy.'.$name]) ? $app['security.session_strategy.'.$name] : $app['security.session_strategy'], + $app['security.http_utils'], + $name, + $app['security.authentication.success_handler.'.$name], + $app['security.authentication.failure_handler.'.$name], + $options, + $app['logger'], + $app['dispatcher'], + isset($options['with_csrf']) && $options['with_csrf'] && isset($app['csrf.token_manager']) ? $app['csrf.token_manager'] : null + ); + }; + }); + + $app['security.authentication_listener.http._proto'] = $app->protect(function ($providerKey, $options) use ($app) { + return function () use ($app, $providerKey, $options) { + return new BasicAuthenticationListener( + $app['security.token_storage'], + $app['security.authentication_manager'], + $providerKey, + $app['security.entry_point.'.$providerKey.'.http'], + $app['logger'] + ); + }; + }); + + $app['security.authentication_listener.anonymous._proto'] = $app->protect(function ($providerKey, $options) use ($app) { + return function () use ($app, $providerKey, $options) { + return new AnonymousAuthenticationListener( + $app['security.token_storage'], + $providerKey, + $app['logger'] + ); + }; + }); + + $app['security.authentication.logout_handler._proto'] = $app->protect(function ($name, $options) use ($app) { + return function () use ($name, $options, $app) { + return new DefaultLogoutSuccessHandler( + $app['security.http_utils'], + isset($options['target_url']) ? $options['target_url'] : '/' + ); + }; + }); + + $app['security.authentication_listener.logout._proto'] = $app->protect(function ($name, $options) use ($app, $that) { + return function () use ($app, $name, $options, $that) { + $that->addFakeRoute( + 'get', + $tmp = isset($options['logout_path']) ? $options['logout_path'] : '/logout', + str_replace('/', '_', ltrim($tmp, '/')) + ); + + if (!isset($app['security.authentication.logout_handler.'.$name])) { + $app['security.authentication.logout_handler.'.$name] = $app['security.authentication.logout_handler._proto']($name, $options); + } + + $listener = new LogoutListener( + $app['security.token_storage'], + $app['security.http_utils'], + $app['security.authentication.logout_handler.'.$name], + $options, + isset($options['with_csrf']) && $options['with_csrf'] && isset($app['csrf.token_manager']) ? $app['csrf.token_manager'] : null + ); + + $invalidateSession = isset($options['invalidate_session']) ? $options['invalidate_session'] : true; + if (true === $invalidateSession && false === $options['stateless']) { + $listener->addHandler(new SessionLogoutHandler()); + } + + return $listener; + }; + }); + + $app['security.authentication_listener.switch_user._proto'] = $app->protect(function ($name, $options) use ($app, $that) { + return function () use ($app, $name, $options, $that) { + return new SwitchUserListener( + $app['security.token_storage'], + $app['security.user_provider.'.$name], + $app['security.user_checker'], + $name, + $app['security.access_manager'], + $app['logger'], + isset($options['parameter']) ? $options['parameter'] : '_switch_user', + isset($options['role']) ? $options['role'] : 'ROLE_ALLOWED_TO_SWITCH', + $app['dispatcher'] + ); + }; + }); + + $app['security.entry_point.form._proto'] = $app->protect(function ($name, array $options) use ($app) { + return function () use ($app, $options) { + $loginPath = isset($options['login_path']) ? $options['login_path'] : '/login'; + $useForward = isset($options['use_forward']) ? $options['use_forward'] : false; + + return new FormAuthenticationEntryPoint($app, $app['security.http_utils'], $loginPath, $useForward); + }; + }); + + $app['security.entry_point.http._proto'] = $app->protect(function ($name, array $options) use ($app) { + return function () use ($app, $name, $options) { + return new BasicAuthenticationEntryPoint(isset($options['real_name']) ? $options['real_name'] : 'Secured'); + }; + }); + + $app['security.entry_point.guard._proto'] = $app->protect(function ($name, array $options) use ($app) { + if (isset($options['entry_point'])) { + // if it's configured explicitly, use it! + return $app[$options['entry_point']]; + } + $authenticatorIds = $options['authenticators']; + if (count($authenticatorIds) == 1) { + // if there is only one authenticator, use that as the entry point + return $app[reset($authenticatorIds)]; + } + // we have multiple entry points - we must ask them to configure one + throw new \LogicException(sprintf( + 'Because you have multiple guard configurators, you need to set the "guard.entry_point" key to one of you configurators (%s)', + implode(', ', $authenticatorIds) + )); + }); + + $app['security.authentication_provider.dao._proto'] = $app->protect(function ($name, $options) use ($app) { + return function () use ($app, $name) { + return new DaoAuthenticationProvider( + $app['security.user_provider.'.$name], + $app['security.user_checker'], + $name, + $app['security.encoder_factory'], + $app['security.hide_user_not_found'] + ); + }; + }); + + $app['security.authentication_provider.guard._proto'] = $app->protect(function ($name, $options) use ($app) { + return function () use ($app, $name, $options) { + $authenticators = array(); + foreach ($options['authenticators'] as $authenticatorId) { + $authenticators[] = $app[$authenticatorId]; + } + + return new GuardAuthenticationProvider( + $authenticators, + $app['security.user_provider.'.$name], + $name, + $app['security.user_checker'] + ); + }; + }); + + $app['security.authentication_provider.anonymous._proto'] = $app->protect(function ($name, $options) use ($app) { + return function () use ($app, $name) { + return new AnonymousAuthenticationProvider($name); + }; + }); + + if (isset($app['validator'])) { + $app['security.validator.user_password_validator'] = function ($app) { + return new UserPasswordValidator($app['security.token_storage'], $app['security.encoder_factory']); + }; + + $app['validator.validator_service_ids'] = array_merge($app['validator.validator_service_ids'], array('security.validator.user_password' => 'security.validator.user_password_validator')); + } + } + + public function subscribe(Container $app, EventDispatcherInterface $dispatcher) + { + $dispatcher->addSubscriber($app['security.firewall']); + } + + public function connect(Application $app) + { + $controllers = $app['controllers_factory']; + foreach ($this->fakeRoutes as $route) { + list($method, $pattern, $name) = $route; + + $controllers->$method($pattern)->run(null)->bind($name); + } + + return $controllers; + } + + public function boot(Application $app) + { + $app->mount('/', $this->connect($app)); + } + + public function addFakeRoute($method, $pattern, $name) + { + $this->fakeRoutes[] = array($method, $pattern, $name); + } +} diff --git a/src/Silex/Provider/SerializerServiceProvider.php b/src/Silex/Provider/SerializerServiceProvider.php new file mode 100644 index 0000000..8986abe --- /dev/null +++ b/src/Silex/Provider/SerializerServiceProvider.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Encoder\XmlEncoder; +use Symfony\Component\Serializer\Normalizer\CustomNormalizer; +use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; + +/** + * Symfony Serializer component Provider. + * + * @author Fabien Potencier + * @author Marijn Huizendveld + */ +class SerializerServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc} + * + * This method registers a serializer service. {@link http://api.symfony.com/master/Symfony/Component/Serializer/Serializer.html + * The service is provided by the Symfony Serializer component}. + */ + public function register(Container $app) + { + $app['serializer'] = function ($app) { + return new Serializer($app['serializer.normalizers'], $app['serializer.encoders']); + }; + + $app['serializer.encoders'] = function () { + return array(new JsonEncoder(), new XmlEncoder()); + }; + + $app['serializer.normalizers'] = function () { + return array(new CustomNormalizer(), new GetSetMethodNormalizer()); + }; + } +} diff --git a/src/Silex/Provider/ServiceControllerServiceProvider.php b/src/Silex/Provider/ServiceControllerServiceProvider.php new file mode 100644 index 0000000..1c38adc --- /dev/null +++ b/src/Silex/Provider/ServiceControllerServiceProvider.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Silex\ServiceControllerResolver; + +class ServiceControllerServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app->extend('resolver', function ($resolver, $app) { + return new ServiceControllerResolver($resolver, $app['callback_resolver']); + }); + } +} diff --git a/src/Silex/Provider/Session/SessionListener.php b/src/Silex/Provider/Session/SessionListener.php new file mode 100644 index 0000000..aba4c4e --- /dev/null +++ b/src/Silex/Provider/Session/SessionListener.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider\Session; + +use Pimple\Container; +use Symfony\Component\HttpKernel\EventListener\SessionListener as BaseSessionListener; + +/** + * Sets the session in the request. + * + * @author Fabien Potencier + */ +class SessionListener extends BaseSessionListener +{ + private $app; + + public function __construct(Container $app) + { + $this->app = $app; + } + + protected function getSession() + { + if (!isset($this->app['session'])) { + return; + } + + return $this->app['session']; + } +} diff --git a/src/Silex/Provider/Session/TestSessionListener.php b/src/Silex/Provider/Session/TestSessionListener.php new file mode 100644 index 0000000..ab98eb1 --- /dev/null +++ b/src/Silex/Provider/Session/TestSessionListener.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider\Session; + +use Pimple\Container; +use Symfony\Component\HttpKernel\EventListener\TestSessionListener as BaseTestSessionListener; + +/** + * Simulates sessions for testing purpose. + * + * @author Fabien Potencier + */ +class TestSessionListener extends BaseTestSessionListener +{ + private $app; + + public function __construct(Container $app) + { + $this->app = $app; + } + + protected function getSession() + { + if (!isset($this->app['session'])) { + return; + } + + return $this->app['session']; + } +} diff --git a/src/Silex/Provider/SessionServiceProvider.php b/src/Silex/Provider/SessionServiceProvider.php new file mode 100644 index 0000000..a51e230 --- /dev/null +++ b/src/Silex/Provider/SessionServiceProvider.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Silex\Api\EventListenerProviderInterface; +use Silex\Provider\Session\SessionListener; +use Silex\Provider\Session\TestSessionListener; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; +use Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage; +use Symfony\Component\HttpFoundation\Session\Session; + +/** + * Symfony HttpFoundation component Provider for sessions. + * + * @author Fabien Potencier + */ +class SessionServiceProvider implements ServiceProviderInterface, EventListenerProviderInterface +{ + public function register(Container $app) + { + $app['session.test'] = false; + + $app['session'] = function ($app) { + return new Session($app['session.storage'], $app['session.attribute_bag'], $app['session.flash_bag']); + }; + + $app['session.storage'] = function ($app) { + if ($app['session.test']) { + return $app['session.storage.test']; + } + + return $app['session.storage.native']; + }; + + $app['session.storage.handler'] = function ($app) { + return new NativeFileSessionHandler($app['session.storage.save_path']); + }; + + $app['session.storage.native'] = function ($app) { + return new NativeSessionStorage( + $app['session.storage.options'], + $app['session.storage.handler'] + ); + }; + + $app['session.listener'] = function ($app) { + return new SessionListener($app); + }; + + $app['session.storage.test'] = function () { + return new MockFileSessionStorage(); + }; + + $app['session.listener.test'] = function ($app) { + return new TestSessionListener($app); + }; + + $app['session.storage.options'] = array(); + $app['session.default_locale'] = 'en'; + $app['session.storage.save_path'] = null; + $app['session.attribute_bag'] = null; + $app['session.flash_bag'] = null; + } + + public function subscribe(Container $app, EventDispatcherInterface $dispatcher) + { + $dispatcher->addSubscriber($app['session.listener']); + + if ($app['session.test']) { + $app['dispatcher']->addSubscriber($app['session.listener.test']); + } + } +} diff --git a/src/Silex/Provider/SwiftmailerServiceProvider.php b/src/Silex/Provider/SwiftmailerServiceProvider.php new file mode 100644 index 0000000..9bd7c78 --- /dev/null +++ b/src/Silex/Provider/SwiftmailerServiceProvider.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Silex\Api\EventListenerProviderInterface; +use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Event\PostResponseEvent; + +/** + * Swiftmailer Provider. + * + * @author Fabien Potencier + */ +class SwiftmailerServiceProvider implements ServiceProviderInterface, EventListenerProviderInterface +{ + public function register(Container $app) + { + $app['swiftmailer.options'] = array(); + $app['swiftmailer.use_spool'] = true; + + $app['mailer.initialized'] = false; + + $app['mailer'] = function ($app) { + $app['mailer.initialized'] = true; + $transport = $app['swiftmailer.use_spool'] ? $app['swiftmailer.spooltransport'] : $app['swiftmailer.transport']; + + return new \Swift_Mailer($transport); + }; + + $app['swiftmailer.spooltransport'] = function ($app) { + return new \Swift_Transport_SpoolTransport($app['swiftmailer.transport.eventdispatcher'], $app['swiftmailer.spool']); + }; + + $app['swiftmailer.spool'] = function ($app) { + return new \Swift_MemorySpool(); + }; + + $app['swiftmailer.transport'] = function ($app) { + $transport = new \Swift_Transport_EsmtpTransport( + $app['swiftmailer.transport.buffer'], + array($app['swiftmailer.transport.authhandler']), + $app['swiftmailer.transport.eventdispatcher'] + ); + + $options = $app['swiftmailer.options'] = array_replace(array( + 'host' => 'localhost', + 'port' => 25, + 'username' => '', + 'password' => '', + 'encryption' => null, + 'auth_mode' => null, + ), $app['swiftmailer.options']); + + $transport->setHost($options['host']); + $transport->setPort($options['port']); + $transport->setEncryption($options['encryption']); + $transport->setUsername($options['username']); + $transport->setPassword($options['password']); + $transport->setAuthMode($options['auth_mode']); + + if (null !== $app['swiftmailer.sender_address']) { + $transport->registerPlugin(new \Swift_Plugins_ImpersonatePlugin($app['swiftmailer.sender_address'])); + } + + if (!empty($app['swiftmailer.delivery_addresses'])) { + $transport->registerPlugin(new \Swift_Plugins_RedirectingPlugin( + $app['swiftmailer.delivery_addresses'], + $app['swiftmailer.delivery_whitelist'] + )); + } + + return $transport; + }; + + $app['swiftmailer.transport.buffer'] = function () { + return new \Swift_Transport_StreamBuffer(new \Swift_StreamFilters_StringReplacementFilterFactory()); + }; + + $app['swiftmailer.transport.authhandler'] = function () { + return new \Swift_Transport_Esmtp_AuthHandler(array( + new \Swift_Transport_Esmtp_Auth_CramMd5Authenticator(), + new \Swift_Transport_Esmtp_Auth_LoginAuthenticator(), + new \Swift_Transport_Esmtp_Auth_PlainAuthenticator(), + )); + }; + + $app['swiftmailer.transport.eventdispatcher'] = function () { + return new \Swift_Events_SimpleEventDispatcher(); + }; + + $app['swiftmailer.sender_address'] = null; + $app['swiftmailer.delivery_addresses'] = []; + $app['swiftmailer.delivery_whitelist'] = []; + } + + public function subscribe(Container $app, EventDispatcherInterface $dispatcher) + { + // Event has no typehint as it can be either a PostResponseEvent or a ConsoleTerminateEvent + $onTerminate = function ($event) use ($app) { + // To speed things up (by avoiding Swift Mailer initialization), flush + // messages only if our mailer has been created (potentially used) + if ($app['mailer.initialized'] && $app['swiftmailer.use_spool'] && $app['swiftmailer.spooltransport'] instanceof \Swift_Transport_SpoolTransport) { + $app['swiftmailer.spooltransport']->getSpool()->flushQueue($app['swiftmailer.transport']); + } + }; + + $dispatcher->addListener(KernelEvents::TERMINATE, $onTerminate); + + if (class_exists('Symfony\Component\Console\ConsoleEvents')) { + $dispatcher->addListener(ConsoleEvents::TERMINATE, $onTerminate); + } + } +} diff --git a/src/Silex/Provider/TranslationServiceProvider.php b/src/Silex/Provider/TranslationServiceProvider.php new file mode 100644 index 0000000..0b8a895 --- /dev/null +++ b/src/Silex/Provider/TranslationServiceProvider.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Symfony\Component\Translation\Translator; +use Symfony\Component\Translation\MessageSelector; +use Symfony\Component\Translation\Loader\ArrayLoader; +use Symfony\Component\Translation\Loader\XliffFileLoader; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpKernel\EventListener\TranslatorListener; +use Silex\Api\EventListenerProviderInterface; + +/** + * Symfony Translation component Provider. + * + * @author Fabien Potencier + */ +class TranslationServiceProvider implements ServiceProviderInterface, EventListenerProviderInterface +{ + public function register(Container $app) + { + $app['translator'] = function ($app) { + if (!isset($app['locale'])) { + throw new \LogicException('You must define \'locale\' parameter or register the LocaleServiceProvider to use the TranslationServiceProvider'); + } + + $translator = new Translator($app['locale'], $app['translator.message_selector'], $app['translator.cache_dir'], $app['debug']); + $translator->setFallbackLocales($app['locale_fallbacks']); + $translator->addLoader('array', new ArrayLoader()); + $translator->addLoader('xliff', new XliffFileLoader()); + + if (isset($app['validator'])) { + $r = new \ReflectionClass('Symfony\Component\Validator\Validation'); + $file = dirname($r->getFilename()).'/Resources/translations/validators.'.$app['locale'].'.xlf'; + if (file_exists($file)) { + $translator->addResource('xliff', $file, $app['locale'], 'validators'); + } + } + + if (isset($app['form.factory'])) { + $r = new \ReflectionClass('Symfony\Component\Form\Form'); + $file = dirname($r->getFilename()).'/Resources/translations/validators.'.$app['locale'].'.xlf'; + if (file_exists($file)) { + $translator->addResource('xliff', $file, $app['locale'], 'validators'); + } + } + + // Register default resources + foreach ($app['translator.resources'] as $resource) { + $translator->addResource($resource[0], $resource[1], $resource[2], $resource[3]); + } + + foreach ($app['translator.domains'] as $domain => $data) { + foreach ($data as $locale => $messages) { + $translator->addResource('array', $messages, $locale, $domain); + } + } + + return $translator; + }; + + if (isset($app['request_stack'])) { + $app['translator.listener'] = function ($app) { + return new TranslatorListener($app['translator'], $app['request_stack']); + }; + } + + $app['translator.message_selector'] = function () { + return new MessageSelector(); + }; + + $app['translator.resources'] = $app->protect(function ($app) { + return array(); + }); + + $app['translator.domains'] = array(); + $app['locale_fallbacks'] = array('en'); + $app['translator.cache_dir'] = null; + } + + public function subscribe(Container $app, EventDispatcherInterface $dispatcher) + { + if (isset($app['translator.listener'])) { + $dispatcher->addSubscriber($app['translator.listener']); + } + } +} diff --git a/src/Silex/Provider/TwigServiceProvider.php b/src/Silex/Provider/TwigServiceProvider.php new file mode 100644 index 0000000..22f7cad --- /dev/null +++ b/src/Silex/Provider/TwigServiceProvider.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Symfony\Bridge\Twig\AppVariable; +use Symfony\Bridge\Twig\Extension\AssetExtension; +use Symfony\Bridge\Twig\Extension\DumpExtension; +use Symfony\Bridge\Twig\Extension\RoutingExtension; +use Symfony\Bridge\Twig\Extension\TranslationExtension; +use Symfony\Bridge\Twig\Extension\FormExtension; +use Symfony\Bridge\Twig\Extension\SecurityExtension; +use Symfony\Bridge\Twig\Extension\HttpFoundationExtension; +use Symfony\Bridge\Twig\Extension\HttpKernelExtension; +use Symfony\Bridge\Twig\Form\TwigRendererEngine; +use Symfony\Bridge\Twig\Form\TwigRenderer; + +/** + * Twig integration for Silex. + * + * @author Fabien Potencier + */ +class TwigServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['twig.options'] = array(); + $app['twig.form.templates'] = array('form_div_layout.html.twig'); + $app['twig.path'] = array(); + $app['twig.templates'] = array(); + + $app['twig.app_variable'] = function ($app) { + $var = new AppVariable(); + if (isset($app['security.token_storage'])) { + $var->setTokenStorage($app['security.token_storage']); + } + if (isset($app['request_stack'])) { + $var->setRequestStack($app['request_stack']); + } + $var->setDebug($app['debug']); + + return $var; + }; + + $app['twig'] = function ($app) { + $app['twig.options'] = array_replace( + array( + 'charset' => $app['charset'], + 'debug' => $app['debug'], + 'strict_variables' => $app['debug'], + ), $app['twig.options'] + ); + + $twig = $app['twig.environment_factory']($app); + // registered for BC, but should not be used anymore + // deprecated and should probably be removed in Silex 3.0 + $twig->addGlobal('app', $app); + + if ($app['debug']) { + $twig->addExtension(new \Twig_Extension_Debug()); + } + + if (class_exists('Symfony\Bridge\Twig\Extension\RoutingExtension')) { + $twig->addGlobal('global', $app['twig.app_variable']); + + if (isset($app['request_stack'])) { + $twig->addExtension(new HttpFoundationExtension($app['request_stack'])); + $twig->addExtension(new RoutingExtension($app['url_generator'])); + } + + if (isset($app['translator'])) { + $twig->addExtension(new TranslationExtension($app['translator'])); + } + + if (isset($app['security.authorization_checker'])) { + $twig->addExtension(new SecurityExtension($app['security.authorization_checker'])); + } + + if (isset($app['fragment.handler'])) { + $app['fragment.renderer.hinclude']->setTemplating($twig); + + $twig->addExtension(new HttpKernelExtension($app['fragment.handler'])); + } + + if (isset($app['assets.packages'])) { + $twig->addExtension(new AssetExtension($app['assets.packages'])); + } + + if (isset($app['form.factory'])) { + $app['twig.form.engine'] = function ($app) { + return new TwigRendererEngine($app['twig.form.templates']); + }; + + $app['twig.form.renderer'] = function ($app) { + $csrfTokenManager = isset($app['csrf.token_manager']) ? $app['csrf.token_manager'] : null; + + return new TwigRenderer($app['twig.form.engine'], $csrfTokenManager); + }; + + $twig->addExtension(new FormExtension($app['twig.form.renderer'])); + + // add loader for Symfony built-in form templates + $reflected = new \ReflectionClass('Symfony\Bridge\Twig\Extension\FormExtension'); + $path = dirname($reflected->getFileName()).'/../Resources/views/Form'; + $app['twig.loader']->addLoader(new \Twig_Loader_Filesystem($path)); + } + + if (isset($app['var_dumper.cloner'])) { + $twig->addExtension(new DumpExtension($app['var_dumper.cloner'])); + } + } + + return $twig; + }; + + $app['twig.loader.filesystem'] = function ($app) { + return new \Twig_Loader_Filesystem($app['twig.path']); + }; + + $app['twig.loader.array'] = function ($app) { + return new \Twig_Loader_Array($app['twig.templates']); + }; + + $app['twig.loader'] = function ($app) { + return new \Twig_Loader_Chain(array( + $app['twig.loader.array'], + $app['twig.loader.filesystem'], + )); + }; + + $app['twig.environment_factory'] = $app->protect(function ($app) { + return new \Twig_Environment($app['twig.loader'], $app['twig.options']); + }); + } +} diff --git a/src/Silex/Provider/Validator/ConstraintValidatorFactory.php b/src/Silex/Provider/Validator/ConstraintValidatorFactory.php new file mode 100644 index 0000000..9f5e499 --- /dev/null +++ b/src/Silex/Provider/Validator/ConstraintValidatorFactory.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider\Validator; + +use Pimple\Container; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidatorFactory as BaseConstraintValidatorFactory; + +/** + * Uses a service container to create constraint validators with dependencies. + * + * @author Kris Wallsmith + * @author Alex Kalyvitis + */ +class ConstraintValidatorFactory extends BaseConstraintValidatorFactory +{ + /** + * @var Container + */ + protected $container; + + /** + * @var array + */ + protected $serviceNames; + + /** + * Constructor. + * + * @param Container $container DI container + * @param array $serviceNames Validator service names + */ + public function __construct(Container $container, array $serviceNames = array(), $propertyAccessor = null) + { + parent::__construct($propertyAccessor); + + $this->container = $container; + $this->serviceNames = $serviceNames; + } + + /** + * {@inheritdoc} + */ + public function getInstance(Constraint $constraint) + { + $name = $constraint->validatedBy(); + + if (isset($this->serviceNames[$name])) { + return $this->container[$this->serviceNames[$name]]; + } + + return parent::getInstance($constraint); + } +} diff --git a/src/Silex/Provider/ValidatorServiceProvider.php b/src/Silex/Provider/ValidatorServiceProvider.php new file mode 100644 index 0000000..d89a3cb --- /dev/null +++ b/src/Silex/Provider/ValidatorServiceProvider.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Silex\Provider\Validator\ConstraintValidatorFactory; +use Symfony\Component\Validator\Validator; +use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory; +use Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader; +use Symfony\Component\Validator\Validation; + +/** + * Symfony Validator component Provider. + * + * @author Fabien Potencier + */ +class ValidatorServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['validator'] = function ($app) { + return $app['validator.builder']->getValidator(); + }; + + $app['validator.builder'] = function ($app) { + $builder = Validation::createValidatorBuilder(); + $builder->setConstraintValidatorFactory($app['validator.validator_factory']); + $builder->setTranslationDomain('validators'); + $builder->addObjectInitializers($app['validator.object_initializers']); + $builder->setMetadataFactory($app['validator.mapping.class_metadata_factory']); + if (isset($app['translator'])) { + $builder->setTranslator($app['translator']); + } + + return $builder; + }; + + $app['validator.mapping.class_metadata_factory'] = function ($app) { + return new LazyLoadingMetadataFactory(new StaticMethodLoader()); + }; + + $app['validator.validator_factory'] = function () use ($app) { + return new ConstraintValidatorFactory($app, $app['validator.validator_service_ids']); + }; + + $app['validator.object_initializers'] = function ($app) { + return array(); + }; + + $app['validator.validator_service_ids'] = array(); + } +} diff --git a/src/Silex/Provider/VarDumperServiceProvider.php b/src/Silex/Provider/VarDumperServiceProvider.php new file mode 100644 index 0000000..7c40b5e --- /dev/null +++ b/src/Silex/Provider/VarDumperServiceProvider.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Silex\Application; +use Silex\Api\BootableProviderInterface; +use Symfony\Component\VarDumper\VarDumper; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Dumper\CliDumper; + +/** + * Symfony Var Dumper component Provider. + * + * @author Fabien Potencier + */ +class VarDumperServiceProvider implements ServiceProviderInterface, BootableProviderInterface +{ + public function register(Container $app) + { + $app['var_dumper.cli_dumper'] = function ($app) { + return new CliDumper($app['var_dumper.dump_destination'], $app['charset']); + }; + + $app['var_dumper.cloner'] = function ($app) { + return new VarCloner(); + }; + + $app['var_dumper.dump_destination'] = null; + } + + public function boot(Application $app) + { + if (!$app['debug']) { + return; + } + + // This code is here to lazy load the dump stack. This default + // configuration for CLI mode is overridden in HTTP mode on + // 'kernel.request' event + VarDumper::setHandler(function ($var) use ($app) { + VarDumper::setHandler($handler = function ($var) use ($app) { + $app['var_dumper.cli_dumper']->dump($app['var_dumper.cloner']->cloneVar($var)); + }); + $handler($var); + }); + } +} diff --git a/src/Silex/Provider/composer.json b/src/Silex/Provider/composer.json new file mode 100644 index 0000000..50d3e5c --- /dev/null +++ b/src/Silex/Provider/composer.json @@ -0,0 +1,31 @@ +{ + "minimum-stability": "dev", + "name": "silex/providers", + "description": "The Silex providers", + "keywords": ["microframework"], + "homepage": "http://silex.sensiolabs.org", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "require": { + "php": ">=5.5.9", + "pimple/pimple": "~3.0", + "silex/api": "~2.0" + }, + "autoload": { + "psr-4": { "Silex\\Provider\\": "" } + }, + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + } +} diff --git a/src/Silex/Route.php b/src/Silex/Route.php new file mode 100644 index 0000000..99e82d8 --- /dev/null +++ b/src/Silex/Route.php @@ -0,0 +1,202 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex; + +use Symfony\Component\Routing\Route as BaseRoute; + +/** + * A wrapper for a controller, mapped to a route. + * + * @author Fabien Potencier + */ +class Route extends BaseRoute +{ + /** + * Constructor. + * + * Available options: + * + * * compiler_class: A class name able to compile this route instance (RouteCompiler by default) + * + * @param string $path The path pattern to match + * @param array $defaults An array of default parameter values + * @param array $requirements An array of requirements for parameters (regexes) + * @param array $options An array of options + * @param string $host The host pattern to match + * @param string|array $schemes A required URI scheme or an array of restricted schemes + * @param string|array $methods A required HTTP method or an array of restricted methods + */ + public function __construct($path = '/', array $defaults = array(), array $requirements = array(), array $options = array(), $host = '', $schemes = array(), $methods = array()) + { + // overridden constructor to make $path optional + parent::__construct($path, $defaults, $requirements, $options, $host, $schemes, $methods); + } + + /** + * Sets the route code that should be executed when matched. + * + * @param callable $to PHP callback that returns the response when matched + * + * @return Route $this The current Route instance + */ + public function run($to) + { + $this->setDefault('_controller', $to); + + return $this; + } + + /** + * Sets the requirement for a route variable. + * + * @param string $variable The variable name + * @param string $regexp The regexp to apply + * + * @return Route $this The current route instance + */ + public function assert($variable, $regexp) + { + $this->setRequirement($variable, $regexp); + + return $this; + } + + /** + * Sets the default value for a route variable. + * + * @param string $variable The variable name + * @param mixed $default The default value + * + * @return Route $this The current Route instance + */ + public function value($variable, $default) + { + $this->setDefault($variable, $default); + + return $this; + } + + /** + * Sets a converter for a route variable. + * + * @param string $variable The variable name + * @param mixed $callback A PHP callback that converts the original value + * + * @return Route $this The current Route instance + */ + public function convert($variable, $callback) + { + $converters = $this->getOption('_converters'); + $converters[$variable] = $callback; + $this->setOption('_converters', $converters); + + return $this; + } + + /** + * Sets the requirement for the HTTP method. + * + * @param string $method The HTTP method name. Multiple methods can be supplied, delimited by a pipe character '|', eg. 'GET|POST' + * + * @return Route $this The current Route instance + */ + public function method($method) + { + $this->setMethods(explode('|', $method)); + + return $this; + } + + /** + * Sets the requirement of host on this Route. + * + * @param string $host The host for which this route should be enabled + * + * @return Route $this The current Route instance + */ + public function host($host) + { + $this->setHost($host); + + return $this; + } + + /** + * Sets the requirement of HTTP (no HTTPS) on this Route. + * + * @return Route $this The current Route instance + */ + public function requireHttp() + { + $this->setSchemes('http'); + + return $this; + } + + /** + * Sets the requirement of HTTPS on this Route. + * + * @return Route $this The current Route instance + */ + public function requireHttps() + { + $this->setSchemes('https'); + + return $this; + } + + /** + * Sets a callback to handle before triggering the route callback. + * + * @param mixed $callback A PHP callback to be triggered when the Route is matched, just before the route callback + * + * @return Route $this The current Route instance + */ + public function before($callback) + { + $callbacks = $this->getOption('_before_middlewares'); + $callbacks[] = $callback; + $this->setOption('_before_middlewares', $callbacks); + + return $this; + } + + /** + * Sets a callback to handle after the route callback. + * + * @param mixed $callback A PHP callback to be triggered after the route callback + * + * @return Route $this The current Route instance + */ + public function after($callback) + { + $callbacks = $this->getOption('_after_middlewares'); + $callbacks[] = $callback; + $this->setOption('_after_middlewares', $callbacks); + + return $this; + } + + /** + * Sets a condition for the route to match. + * + * @param string $condition The condition + * + * @return Route $this The current Route instance + */ + public function when($condition) + { + $this->setCondition($condition); + + return $this; + } +} diff --git a/src/Silex/Route/SecurityTrait.php b/src/Silex/Route/SecurityTrait.php new file mode 100644 index 0000000..d42ba2f --- /dev/null +++ b/src/Silex/Route/SecurityTrait.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Route; + +use Symfony\Component\Security\Core\Exception\AccessDeniedException; + +/** + * Security trait. + * + * @author Fabien Potencier + */ +trait SecurityTrait +{ + public function secure($roles) + { + $this->before(function ($request, $app) use ($roles) { + if (!$app['security.authorization_checker']->isGranted($roles)) { + throw new AccessDeniedException(); + } + }); + } +} diff --git a/src/Silex/ServiceControllerResolver.php b/src/Silex/ServiceControllerResolver.php new file mode 100644 index 0000000..87f91b0 --- /dev/null +++ b/src/Silex/ServiceControllerResolver.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; + +/** + * Enables name_of_service:method_name syntax for declaring controllers. + * + * @link http://silex.sensiolabs.org/doc/providers/service_controller.html + */ +class ServiceControllerResolver implements ControllerResolverInterface +{ + protected $controllerResolver; + protected $callbackResolver; + + /** + * Constructor. + * + * @param ControllerResolverInterface $controllerResolver A ControllerResolverInterface instance to delegate to + * @param CallbackResolver $callbackResolver A service resolver instance + */ + public function __construct(ControllerResolverInterface $controllerResolver, CallbackResolver $callbackResolver) + { + $this->controllerResolver = $controllerResolver; + $this->callbackResolver = $callbackResolver; + } + + /** + * {@inheritdoc} + */ + public function getController(Request $request) + { + $controller = $request->attributes->get('_controller', null); + + if (!$this->callbackResolver->isValid($controller)) { + return $this->controllerResolver->getController($request); + } + + return $this->callbackResolver->convertCallback($controller); + } + + /** + * {@inheritdoc} + */ + public function getArguments(Request $request, $controller) + { + return $this->controllerResolver->getArguments($request, $controller); + } +} diff --git a/src/Silex/ViewListenerWrapper.php b/src/Silex/ViewListenerWrapper.php new file mode 100644 index 0000000..a67ec93 --- /dev/null +++ b/src/Silex/ViewListenerWrapper.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; + +/** + * Wraps view listeners. + * + * @author Dave Marshall + */ +class ViewListenerWrapper +{ + private $app; + private $callback; + + /** + * Constructor. + * + * @param Application $app An Application instance + * @param mixed $callback + */ + public function __construct(Application $app, $callback) + { + $this->app = $app; + $this->callback = $callback; + } + + public function __invoke(GetResponseForControllerResultEvent $event) + { + $controllerResult = $event->getControllerResult(); + $callback = $this->app['callback_resolver']->resolveCallback($this->callback); + + if (!$this->shouldRun($callback, $controllerResult)) { + return; + } + + $response = call_user_func($callback, $controllerResult, $event->getRequest()); + + if ($response instanceof Response) { + $event->setResponse($response); + } elseif (null !== $response) { + $event->setControllerResult($response); + } + } + + private function shouldRun($callback, $controllerResult) + { + if (is_array($callback)) { + $callbackReflection = new \ReflectionMethod($callback[0], $callback[1]); + } elseif (is_object($callback) && !$callback instanceof \Closure) { + $callbackReflection = new \ReflectionObject($callback); + $callbackReflection = $callbackReflection->getMethod('__invoke'); + } else { + $callbackReflection = new \ReflectionFunction($callback); + } + + if ($callbackReflection->getNumberOfParameters() > 0) { + $parameters = $callbackReflection->getParameters(); + $expectedControllerResult = $parameters[0]; + + if ($expectedControllerResult->getClass() && (!is_object($controllerResult) || !$expectedControllerResult->getClass()->isInstance($controllerResult))) { + return false; + } + + if ($expectedControllerResult->isArray() && !is_array($controllerResult)) { + return false; + } + + if (method_exists($expectedControllerResult, 'isCallable') && $expectedControllerResult->isCallable() && !is_callable($controllerResult)) { + return false; + } + } + + return true; + } +} diff --git a/src/Silex/WebTestCase.php b/src/Silex/WebTestCase.php new file mode 100644 index 0000000..644bb05 --- /dev/null +++ b/src/Silex/WebTestCase.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex; + +use Symfony\Component\HttpKernel\Client; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +/** + * WebTestCase is the base class for functional tests. + * + * @author Igor Wiedler + */ +abstract class WebTestCase extends \PHPUnit_Framework_TestCase +{ + /** + * HttpKernelInterface instance. + * + * @var HttpKernelInterface + */ + protected $app; + + /** + * PHPUnit setUp for setting up the application. + * + * Note: Child classes that define a setUp method must call + * parent::setUp(). + */ + protected function setUp() + { + $this->app = $this->createApplication(); + } + + /** + * Creates the application. + * + * @return HttpKernelInterface + */ + abstract public function createApplication(); + + /** + * Creates a Client. + * + * @param array $server Server parameters + * + * @return Client A Client instance + */ + public function createClient(array $server = array()) + { + if (!class_exists('Symfony\Component\BrowserKit\Client')) { + throw new \LogicException('Component "symfony/browser-kit" is required by WebTestCase.'.PHP_EOL.'Run composer require symfony/browser-kit'); + } + + return new Client($this->app, $server); + } +} diff --git a/tests/Silex/Tests/Application/FormApplication.php b/tests/Silex/Tests/Application/FormApplication.php new file mode 100644 index 0000000..5851a4c --- /dev/null +++ b/tests/Silex/Tests/Application/FormApplication.php @@ -0,0 +1,19 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Application; + +use Silex\Application; + +class FormApplication extends Application +{ + use Application\FormTrait; +} diff --git a/tests/Silex/Tests/Application/FormTraitTest.php b/tests/Silex/Tests/Application/FormTraitTest.php new file mode 100644 index 0000000..74e56c0 --- /dev/null +++ b/tests/Silex/Tests/Application/FormTraitTest.php @@ -0,0 +1,35 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Application; + +use Silex\Provider\FormServiceProvider; + +/** + * FormTrait test cases. + * + * @author Fabien Potencier + */ +class FormTraitTest extends \PHPUnit_Framework_TestCase +{ + public function testForm() + { + $this->assertInstanceOf('Symfony\Component\Form\FormBuilder', $this->createApplication()->form()); + } + + public function createApplication() + { + $app = new FormApplication(); + $app->register(new FormServiceProvider()); + + return $app; + } +} diff --git a/tests/Silex/Tests/Application/MonologApplication.php b/tests/Silex/Tests/Application/MonologApplication.php new file mode 100644 index 0000000..9fec12f --- /dev/null +++ b/tests/Silex/Tests/Application/MonologApplication.php @@ -0,0 +1,19 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Application; + +use Silex\Application; + +class MonologApplication extends Application +{ + use Application\MonologTrait; +} diff --git a/tests/Silex/Tests/Application/MonologTraitTest.php b/tests/Silex/Tests/Application/MonologTraitTest.php new file mode 100644 index 0000000..a2e3acb --- /dev/null +++ b/tests/Silex/Tests/Application/MonologTraitTest.php @@ -0,0 +1,47 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Application; + +use Silex\Provider\MonologServiceProvider; +use Monolog\Handler\TestHandler; +use Monolog\Logger; + +/** + * MonologTrait test cases. + * + * @author Fabien Potencier + */ +class MonologTraitTest extends \PHPUnit_Framework_TestCase +{ + public function testLog() + { + $app = $this->createApplication(); + + $app->log('Foo'); + $app->log('Bar', array(), Logger::DEBUG); + $this->assertTrue($app['monolog.handler']->hasInfo('Foo')); + $this->assertTrue($app['monolog.handler']->hasDebug('Bar')); + } + + public function createApplication() + { + $app = new MonologApplication(); + $app->register(new MonologServiceProvider(), array( + 'monolog.handler' => function () use ($app) { + return new TestHandler($app['monolog.level']); + }, + 'monolog.logfile' => 'php://memory', + )); + + return $app; + } +} diff --git a/tests/Silex/Tests/Application/SecurityApplication.php b/tests/Silex/Tests/Application/SecurityApplication.php new file mode 100644 index 0000000..dc85999 --- /dev/null +++ b/tests/Silex/Tests/Application/SecurityApplication.php @@ -0,0 +1,19 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Application; + +use Silex\Application; + +class SecurityApplication extends Application +{ + use Application\SecurityTrait; +} diff --git a/tests/Silex/Tests/Application/SecurityTraitTest.php b/tests/Silex/Tests/Application/SecurityTraitTest.php new file mode 100644 index 0000000..e91eda7 --- /dev/null +++ b/tests/Silex/Tests/Application/SecurityTraitTest.php @@ -0,0 +1,90 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Application; + +use Silex\Provider\SecurityServiceProvider; +use Symfony\Component\Security\Core\User\User; +use Symfony\Component\HttpFoundation\Request; + +/** + * SecurityTrait test cases. + * + * @author Fabien Potencier + */ +class SecurityTraitTest extends \PHPUnit_Framework_TestCase +{ + public function testEncodePassword() + { + $app = $this->createApplication(array( + 'fabien' => array('ROLE_ADMIN', '$2y$15$lzUNsTegNXvZW3qtfucV0erYBcEqWVeyOmjolB7R1uodsAVJ95vvu'), + )); + + $user = new User('foo', 'bar'); + $password = 'foo'; + $encoded = $app->encodePassword($user, $password); + + $this->assertTrue( + $app['security.encoder_factory']->getEncoder($user)->isPasswordValid($encoded, $password, $user->getSalt()) + ); + } + + /** + * @expectedException \Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException + */ + public function testIsGrantedWithoutTokenThrowsException() + { + $app = $this->createApplication(); + $app->get('/', function () { return 'foo'; }); + $app->handle(Request::create('/')); + $app->isGranted('ROLE_ADMIN'); + } + + public function testIsGranted() + { + $request = Request::create('/'); + + $app = $this->createApplication(array( + 'fabien' => array('ROLE_ADMIN', '$2y$15$lzUNsTegNXvZW3qtfucV0erYBcEqWVeyOmjolB7R1uodsAVJ95vvu'), + 'monique' => array('ROLE_USER', '$2y$15$lzUNsTegNXvZW3qtfucV0erYBcEqWVeyOmjolB7R1uodsAVJ95vvu'), + )); + $app->get('/', function () { return 'foo'; }); + + // User is Monique (ROLE_USER) + $request->headers->set('PHP_AUTH_USER', 'monique'); + $request->headers->set('PHP_AUTH_PW', 'foo'); + $app->handle($request); + $this->assertTrue($app->isGranted('ROLE_USER')); + $this->assertFalse($app->isGranted('ROLE_ADMIN')); + + // User is Fabien (ROLE_ADMIN) + $request->headers->set('PHP_AUTH_USER', 'fabien'); + $request->headers->set('PHP_AUTH_PW', 'foo'); + $app->handle($request); + $this->assertFalse($app->isGranted('ROLE_USER')); + $this->assertTrue($app->isGranted('ROLE_ADMIN')); + } + + public function createApplication($users = array()) + { + $app = new SecurityApplication(); + $app->register(new SecurityServiceProvider(), array( + 'security.firewalls' => array( + 'default' => array( + 'http' => true, + 'users' => $users, + ), + ), + )); + + return $app; + } +} diff --git a/tests/Silex/Tests/Application/SwiftmailerApplication.php b/tests/Silex/Tests/Application/SwiftmailerApplication.php new file mode 100644 index 0000000..6a28d53 --- /dev/null +++ b/tests/Silex/Tests/Application/SwiftmailerApplication.php @@ -0,0 +1,19 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Application; + +use Silex\Application; + +class SwiftmailerApplication extends Application +{ + use Application\SwiftmailerTrait; +} diff --git a/tests/Silex/Tests/Application/SwiftmailerTraitTest.php b/tests/Silex/Tests/Application/SwiftmailerTraitTest.php new file mode 100644 index 0000000..923db39 --- /dev/null +++ b/tests/Silex/Tests/Application/SwiftmailerTraitTest.php @@ -0,0 +1,44 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Application; + +use Silex\Provider\SwiftmailerServiceProvider; + +/** + * SwiftmailerTrait test cases. + * + * @author Fabien Potencier + */ +class SwiftmailerTraitTest extends \PHPUnit_Framework_TestCase +{ + public function testMail() + { + $app = $this->createApplication(); + + $message = $this->getMockBuilder('Swift_Message')->disableOriginalConstructor()->getMock(); + $app['mailer'] = $mailer = $this->getMockBuilder('Swift_Mailer')->disableOriginalConstructor()->getMock(); + $mailer->expects($this->once()) + ->method('send') + ->with($message) + ; + + $app->mail($message); + } + + public function createApplication() + { + $app = new SwiftmailerApplication(); + $app->register(new SwiftmailerServiceProvider()); + + return $app; + } +} diff --git a/tests/Silex/Tests/Application/TranslationApplication.php b/tests/Silex/Tests/Application/TranslationApplication.php new file mode 100644 index 0000000..3e51b9c --- /dev/null +++ b/tests/Silex/Tests/Application/TranslationApplication.php @@ -0,0 +1,19 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Application; + +use Silex\Application; + +class TranslationApplication extends Application +{ + use Application\TranslationTrait; +} diff --git a/tests/Silex/Tests/Application/TranslationTraitTest.php b/tests/Silex/Tests/Application/TranslationTraitTest.php new file mode 100644 index 0000000..f2837c1 --- /dev/null +++ b/tests/Silex/Tests/Application/TranslationTraitTest.php @@ -0,0 +1,46 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Application; + +use Silex\Provider\TranslationServiceProvider; + +/** + * TranslationTrait test cases. + * + * @author Fabien Potencier + */ +class TranslationTraitTest extends \PHPUnit_Framework_TestCase +{ + public function testTrans() + { + $app = $this->createApplication(); + $app['translator'] = $translator = $this->getMockBuilder('Symfony\Component\Translation\Translator')->disableOriginalConstructor()->getMock(); + $translator->expects($this->once())->method('trans'); + $app->trans('foo'); + } + + public function testTransChoice() + { + $app = $this->createApplication(); + $app['translator'] = $translator = $this->getMockBuilder('Symfony\Component\Translation\Translator')->disableOriginalConstructor()->getMock(); + $translator->expects($this->once())->method('transChoice'); + $app->transChoice('foo', 2); + } + + public function createApplication() + { + $app = new TranslationApplication(); + $app->register(new TranslationServiceProvider()); + + return $app; + } +} diff --git a/tests/Silex/Tests/Application/TwigApplication.php b/tests/Silex/Tests/Application/TwigApplication.php new file mode 100644 index 0000000..f1bb473 --- /dev/null +++ b/tests/Silex/Tests/Application/TwigApplication.php @@ -0,0 +1,19 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Application; + +use Silex\Application; + +class TwigApplication extends Application +{ + use Application\TwigTrait; +} diff --git a/tests/Silex/Tests/Application/TwigTraitTest.php b/tests/Silex/Tests/Application/TwigTraitTest.php new file mode 100644 index 0000000..9435f7c --- /dev/null +++ b/tests/Silex/Tests/Application/TwigTraitTest.php @@ -0,0 +1,80 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Application; + +use Silex\Provider\TwigServiceProvider; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedResponse; + +/** + * TwigTrait test cases. + * + * @author Fabien Potencier + */ +class TwigTraitTest extends \PHPUnit_Framework_TestCase +{ + public function testRender() + { + $app = $this->createApplication(); + + $app['twig'] = $mailer = $this->getMockBuilder('Twig_Environment')->disableOriginalConstructor()->getMock(); + $mailer->expects($this->once())->method('render')->will($this->returnValue('foo')); + + $response = $app->render('view'); + $this->assertEquals('Symfony\Component\HttpFoundation\Response', get_class($response)); + $this->assertEquals('foo', $response->getContent()); + } + + public function testRenderKeepResponse() + { + $app = $this->createApplication(); + + $app['twig'] = $mailer = $this->getMockBuilder('Twig_Environment')->disableOriginalConstructor()->getMock(); + $mailer->expects($this->once())->method('render')->will($this->returnValue('foo')); + + $response = $app->render('view', array(), new Response('', 404)); + $this->assertEquals(404, $response->getStatusCode()); + } + + public function testRenderForStream() + { + $app = $this->createApplication(); + + $app['twig'] = $mailer = $this->getMockBuilder('Twig_Environment')->disableOriginalConstructor()->getMock(); + $mailer->expects($this->once())->method('display')->will($this->returnCallback(function () { echo 'foo'; })); + + $response = $app->render('view', array(), new StreamedResponse()); + $this->assertEquals('Symfony\Component\HttpFoundation\StreamedResponse', get_class($response)); + + ob_start(); + $response->send(); + $this->assertEquals('foo', ob_get_clean()); + } + + public function testRenderView() + { + $app = $this->createApplication(); + + $app['twig'] = $mailer = $this->getMockBuilder('Twig_Environment')->disableOriginalConstructor()->getMock(); + $mailer->expects($this->once())->method('render'); + + $app->renderView('view'); + } + + public function createApplication() + { + $app = new TwigApplication(); + $app->register(new TwigServiceProvider()); + + return $app; + } +} diff --git a/tests/Silex/Tests/Application/UrlGeneratorApplication.php b/tests/Silex/Tests/Application/UrlGeneratorApplication.php new file mode 100644 index 0000000..4239af4 --- /dev/null +++ b/tests/Silex/Tests/Application/UrlGeneratorApplication.php @@ -0,0 +1,19 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Application; + +use Silex\Application; + +class UrlGeneratorApplication extends Application +{ + use Application\UrlGeneratorTrait; +} diff --git a/tests/Silex/Tests/Application/UrlGeneratorTraitTest.php b/tests/Silex/Tests/Application/UrlGeneratorTraitTest.php new file mode 100644 index 0000000..822c6eb --- /dev/null +++ b/tests/Silex/Tests/Application/UrlGeneratorTraitTest.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Application; + +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + +/** + * UrlGeneratorTrait test cases. + * + * @author Fabien Potencier + */ +class UrlGeneratorTraitTest extends \PHPUnit_Framework_TestCase +{ + public function testUrl() + { + $app = new UrlGeneratorApplication(); + $app['url_generator'] = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->disableOriginalConstructor()->getMock(); + $app['url_generator']->expects($this->once())->method('generate')->with('foo', array(), UrlGeneratorInterface::ABSOLUTE_URL); + $app->url('foo'); + } + + public function testPath() + { + $app = new UrlGeneratorApplication(); + $app['url_generator'] = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->disableOriginalConstructor()->getMock(); + $app['url_generator']->expects($this->once())->method('generate')->with('foo', array(), UrlGeneratorInterface::ABSOLUTE_PATH); + $app->path('foo'); + } +} diff --git a/tests/Silex/Tests/ApplicationTest.php b/tests/Silex/Tests/ApplicationTest.php new file mode 100644 index 0000000..65175a5 --- /dev/null +++ b/tests/Silex/Tests/ApplicationTest.php @@ -0,0 +1,701 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests; + +use Silex\Application; +use Silex\ControllerCollection; +use Silex\Api\ControllerProviderInterface; +use Silex\Route; +use Silex\Provider\MonologServiceProvider; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\Debug\ErrorHandler; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\EventDispatcher\Event; +use Symfony\Component\Routing\RouteCollection; + +/** + * Application test cases. + * + * @author Igor Wiedler + */ +class ApplicationTest extends \PHPUnit_Framework_TestCase +{ + public function testMatchReturnValue() + { + $app = new Application(); + + $returnValue = $app->match('/foo', function () {}); + $this->assertInstanceOf('Silex\Controller', $returnValue); + + $returnValue = $app->get('/foo', function () {}); + $this->assertInstanceOf('Silex\Controller', $returnValue); + + $returnValue = $app->post('/foo', function () {}); + $this->assertInstanceOf('Silex\Controller', $returnValue); + + $returnValue = $app->put('/foo', function () {}); + $this->assertInstanceOf('Silex\Controller', $returnValue); + + $returnValue = $app->patch('/foo', function () {}); + $this->assertInstanceOf('Silex\Controller', $returnValue); + + $returnValue = $app->delete('/foo', function () {}); + $this->assertInstanceOf('Silex\Controller', $returnValue); + } + + public function testConstructorInjection() + { + // inject a custom parameter + $params = array('param' => 'value'); + $app = new Application($params); + $this->assertSame($params['param'], $app['param']); + + // inject an existing parameter + $params = array('locale' => 'value'); + $app = new Application($params); + $this->assertSame($params['locale'], $app['locale']); + } + + public function testGetRequest() + { + $request = Request::create('/'); + + $app = new Application(); + $app->get('/', function (Request $req) use ($request) { + return $request === $req ? 'ok' : 'ko'; + }); + + $this->assertEquals('ok', $app->handle($request)->getContent()); + } + + public function testGetRoutesWithNoRoutes() + { + $app = new Application(); + + $routes = $app['routes']; + $this->assertInstanceOf('Symfony\Component\Routing\RouteCollection', $routes); + $this->assertEquals(0, count($routes->all())); + } + + public function testGetRoutesWithRoutes() + { + $app = new Application(); + + $app->get('/foo', function () { + return 'foo'; + }); + + $app->get('/bar')->run(function () { + return 'bar'; + }); + + $routes = $app['routes']; + $this->assertInstanceOf('Symfony\Component\Routing\RouteCollection', $routes); + $this->assertEquals(0, count($routes->all())); + $app->flush(); + $this->assertEquals(2, count($routes->all())); + } + + public function testOnCoreController() + { + $app = new Application(); + + $app->get('/foo/{foo}', function (\ArrayObject $foo) { + return $foo['foo']; + })->convert('foo', function ($foo) { return new \ArrayObject(array('foo' => $foo)); }); + + $response = $app->handle(Request::create('/foo/bar')); + $this->assertEquals('bar', $response->getContent()); + + $app->get('/foo/{foo}/{bar}', function (\ArrayObject $foo) { + return $foo['foo']; + })->convert('foo', function ($foo, Request $request) { return new \ArrayObject(array('foo' => $foo.$request->attributes->get('bar'))); }); + + $response = $app->handle(Request::create('/foo/foo/bar')); + $this->assertEquals('foobar', $response->getContent()); + } + + public function testOn() + { + $app = new Application(); + $app['pass'] = false; + + $app->on('test', function (Event $e) use ($app) { + $app['pass'] = true; + }); + + $app['dispatcher']->dispatch('test'); + + $this->assertTrue($app['pass']); + } + + public function testAbort() + { + $app = new Application(); + + try { + $app->abort(404); + $this->fail(); + } catch (HttpException $e) { + $this->assertEquals(404, $e->getStatusCode()); + } + } + + /** + * @dataProvider escapeProvider + */ + public function testEscape($expected, $text) + { + $app = new Application(); + + $this->assertEquals($expected, $app->escape($text)); + } + + public function escapeProvider() + { + return array( + array('<', '<'), + array('>', '>'), + array('"', '"'), + array("'", "'"), + array('abc', 'abc'), + ); + } + + public function testControllersAsMethods() + { + $app = new Application(); + + $app->get('/{name}', 'Silex\Tests\FooController::barAction'); + + $this->assertEquals('Hello Fabien', $app->handle(Request::create('/Fabien'))->getContent()); + } + + public function testApplicationTypeHintWorks() + { + $app = new SpecialApplication(); + + $app->get('/{name}', 'Silex\Tests\FooController::barSpecialAction'); + + $this->assertEquals('Hello Fabien in Silex\Tests\SpecialApplication', $app->handle(Request::create('/Fabien'))->getContent()); + } + + public function testHttpSpec() + { + $app = new Application(); + $app['charset'] = 'ISO-8859-1'; + + $app->get('/', function () { + return 'hello'; + }); + + // content is empty for HEAD requests + $response = $app->handle(Request::create('/', 'HEAD')); + $this->assertEquals('', $response->getContent()); + + // charset is appended to Content-Type + $response = $app->handle(Request::create('/')); + + $this->assertEquals('text/html; charset=ISO-8859-1', $response->headers->get('Content-Type')); + } + + public function testRoutesMiddlewares() + { + $app = new Application(); + + $test = $this; + + $middlewareTarget = array(); + $beforeMiddleware1 = function (Request $request) use (&$middlewareTarget, $test) { + $test->assertEquals('/reached', $request->getRequestUri()); + $middlewareTarget[] = 'before_middleware1_triggered'; + }; + $beforeMiddleware2 = function (Request $request) use (&$middlewareTarget, $test) { + $test->assertEquals('/reached', $request->getRequestUri()); + $middlewareTarget[] = 'before_middleware2_triggered'; + }; + $beforeMiddleware3 = function (Request $request) use (&$middlewareTarget, $test) { + throw new \Exception('This middleware shouldn\'t run!'); + }; + + $afterMiddleware1 = function (Request $request, Response $response) use (&$middlewareTarget, $test) { + $test->assertEquals('/reached', $request->getRequestUri()); + $middlewareTarget[] = 'after_middleware1_triggered'; + }; + $afterMiddleware2 = function (Request $request, Response $response) use (&$middlewareTarget, $test) { + $test->assertEquals('/reached', $request->getRequestUri()); + $middlewareTarget[] = 'after_middleware2_triggered'; + }; + $afterMiddleware3 = function (Request $request, Response $response) use (&$middlewareTarget, $test) { + throw new \Exception('This middleware shouldn\'t run!'); + }; + + $app->get('/reached', function () use (&$middlewareTarget) { + $middlewareTarget[] = 'route_triggered'; + + return 'hello'; + }) + ->before($beforeMiddleware1) + ->before($beforeMiddleware2) + ->after($afterMiddleware1) + ->after($afterMiddleware2); + + $app->get('/never-reached', function () use (&$middlewareTarget) { + throw new \Exception('This route shouldn\'t run!'); + }) + ->before($beforeMiddleware3) + ->after($afterMiddleware3); + + $result = $app->handle(Request::create('/reached')); + + $this->assertSame(array('before_middleware1_triggered', 'before_middleware2_triggered', 'route_triggered', 'after_middleware1_triggered', 'after_middleware2_triggered'), $middlewareTarget); + $this->assertEquals('hello', $result->getContent()); + } + + public function testRoutesBeforeMiddlewaresWithResponseObject() + { + $app = new Application(); + + $app->get('/foo', function () { + throw new \Exception('This route shouldn\'t run!'); + }) + ->before(function () { + return new Response('foo'); + }); + + $request = Request::create('/foo'); + $result = $app->handle($request); + + $this->assertEquals('foo', $result->getContent()); + } + + public function testRoutesAfterMiddlewaresWithResponseObject() + { + $app = new Application(); + + $app->get('/foo', function () { + return new Response('foo'); + }) + ->after(function () { + return new Response('bar'); + }); + + $request = Request::create('/foo'); + $result = $app->handle($request); + + $this->assertEquals('bar', $result->getContent()); + } + + public function testRoutesBeforeMiddlewaresWithRedirectResponseObject() + { + $app = new Application(); + + $app->get('/foo', function () { + throw new \Exception('This route shouldn\'t run!'); + }) + ->before(function () use ($app) { + return $app->redirect('/bar'); + }); + + $request = Request::create('/foo'); + $result = $app->handle($request); + + $this->assertInstanceOf('Symfony\Component\HttpFoundation\RedirectResponse', $result); + $this->assertEquals('/bar', $result->getTargetUrl()); + } + + public function testRoutesBeforeMiddlewaresTriggeredAfterSilexBeforeFilters() + { + $app = new Application(); + + $middlewareTarget = array(); + $middleware = function (Request $request) use (&$middlewareTarget) { + $middlewareTarget[] = 'middleware_triggered'; + }; + + $app->get('/foo', function () use (&$middlewareTarget) { + $middlewareTarget[] = 'route_triggered'; + }) + ->before($middleware); + + $app->before(function () use (&$middlewareTarget) { + $middlewareTarget[] = 'before_triggered'; + }); + + $app->handle(Request::create('/foo')); + + $this->assertSame(array('before_triggered', 'middleware_triggered', 'route_triggered'), $middlewareTarget); + } + + public function testRoutesAfterMiddlewaresTriggeredBeforeSilexAfterFilters() + { + $app = new Application(); + + $middlewareTarget = array(); + $middleware = function (Request $request) use (&$middlewareTarget) { + $middlewareTarget[] = 'middleware_triggered'; + }; + + $app->get('/foo', function () use (&$middlewareTarget) { + $middlewareTarget[] = 'route_triggered'; + }) + ->after($middleware); + + $app->after(function () use (&$middlewareTarget) { + $middlewareTarget[] = 'after_triggered'; + }); + + $app->handle(Request::create('/foo')); + + $this->assertSame(array('route_triggered', 'middleware_triggered', 'after_triggered'), $middlewareTarget); + } + + public function testFinishFilter() + { + $containerTarget = array(); + + $app = new Application(); + + $app->finish(function () use (&$containerTarget) { + $containerTarget[] = '4_filterFinish'; + }); + + $app->get('/foo', function () use (&$containerTarget) { + $containerTarget[] = '1_routeTriggered'; + + return new StreamedResponse(function () use (&$containerTarget) { + $containerTarget[] = '3_responseSent'; + }); + }); + + $app->after(function () use (&$containerTarget) { + $containerTarget[] = '2_filterAfter'; + }); + + $app->run(Request::create('/foo')); + + $this->assertSame(array('1_routeTriggered', '2_filterAfter', '3_responseSent', '4_filterFinish'), $containerTarget); + } + + /** + * @expectedException \RuntimeException + */ + public function testNonResponseAndNonNullReturnFromRouteBeforeMiddlewareShouldThrowRuntimeException() + { + $app = new Application(); + + $middleware = function (Request $request) { + return 'string return'; + }; + + $app->get('/', function () { + return 'hello'; + }) + ->before($middleware); + + $app->handle(Request::create('/'), HttpKernelInterface::MASTER_REQUEST, false); + } + + /** + * @expectedException \RuntimeException + */ + public function testNonResponseAndNonNullReturnFromRouteAfterMiddlewareShouldThrowRuntimeException() + { + $app = new Application(); + + $middleware = function (Request $request) { + return 'string return'; + }; + + $app->get('/', function () { + return 'hello'; + }) + ->after($middleware); + + $app->handle(Request::create('/'), HttpKernelInterface::MASTER_REQUEST, false); + } + + public function testSubRequest() + { + $app = new Application(); + $app->get('/sub', function (Request $request) { + return new Response('foo'); + }); + $app->get('/', function (Request $request) use ($app) { + return $app->handle(Request::create('/sub'), HttpKernelInterface::SUB_REQUEST); + }); + + $this->assertEquals('foo', $app->handle(Request::create('/'))->getContent()); + } + + public function testRegisterShouldReturnSelf() + { + $app = new Application(); + $provider = $this->getMock('Pimple\ServiceProviderInterface'); + + $this->assertSame($app, $app->register($provider)); + } + + public function testMountShouldReturnSelf() + { + $app = new Application(); + $mounted = new ControllerCollection(new Route()); + $mounted->get('/{name}', function ($name) { return new Response($name); }); + + $this->assertSame($app, $app->mount('/hello', $mounted)); + } + + public function testMountPreservesOrder() + { + $app = new Application(); + $mounted = new ControllerCollection(new Route()); + $mounted->get('/mounted')->bind('second'); + + $app->get('/before')->bind('first'); + $app->mount('/', $mounted); + $app->get('/after')->bind('third'); + $app->flush(); + + $this->assertEquals(array('first', 'second', 'third'), array_keys(iterator_to_array($app['routes']))); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage The "mount" method takes either a "ControllerCollection" instance, "ControllerProviderInterface" instance, or a callable. + */ + public function testMountNullException() + { + $app = new Application(); + $app->mount('/exception', null); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage The method "Silex\Tests\IncorrectControllerCollection::connect" must return a "ControllerCollection" instance. Got: "NULL" + */ + public function testMountWrongConnectReturnValueException() + { + $app = new Application(); + $app->mount('/exception', new IncorrectControllerCollection()); + } + + public function testMountCallable() + { + $app = new Application(); + $app->mount('/prefix', function (ControllerCollection $coll) { + $coll->get('/path'); + }); + + $app->flush(); + + $this->assertEquals(1, $app['routes']->count()); + } + + public function testSendFile() + { + $app = new Application(); + + $response = $app->sendFile(__FILE__, 200, array('Content-Type: application/php')); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\BinaryFileResponse', $response); + $this->assertEquals(__FILE__, (string) $response->getFile()); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage The "homepage" route must have code to run when it matches. + */ + public function testGetRouteCollectionWithRouteWithoutController() + { + $app = new Application(); + unset($app['exception_handler']); + $app->match('/')->bind('homepage'); + $app->handle(Request::create('/')); + } + + public function testRedirectDoesNotRaisePHPNoticesWhenMonologIsRegistered() + { + $app = new Application(); + + ErrorHandler::register(null, false); + $app['monolog.logfile'] = 'php://memory'; + $app->register(new MonologServiceProvider()); + $app->get('/foo/', function () { return 'ok'; }); + + $response = $app->handle(Request::create('/foo')); + $this->assertEquals(301, $response->getStatusCode()); + } + + public function testBeforeFilterOnMountedControllerGroupIsolatedToGroup() + { + $app = new Application(); + $app->match('/', function () { return new Response('ok'); }); + $mounted = $app['controllers_factory']; + $mounted->before(function () { return new Response('not ok'); }); + $app->mount('/group', $mounted); + + $response = $app->handle(Request::create('/')); + $this->assertEquals('ok', $response->getContent()); + } + + public function testViewListenerWithPrimitive() + { + $app = new Application(); + $app->get('/foo', function () { return 123; }); + $app->view(function ($view, Request $request) { + return new Response($view); + }); + + $response = $app->handle(Request::create('/foo')); + + $this->assertEquals('123', $response->getContent()); + } + + public function testViewListenerWithArrayTypeHint() + { + $app = new Application(); + $app->get('/foo', function () { return array('ok'); }); + $app->view(function (array $view) { + return new Response($view[0]); + }); + + $response = $app->handle(Request::create('/foo')); + + $this->assertEquals('ok', $response->getContent()); + } + + public function testViewListenerWithObjectTypeHint() + { + $app = new Application(); + $app->get('/foo', function () { return (object) array('name' => 'world'); }); + $app->view(function (\stdClass $view) { + return new Response('Hello '.$view->name); + }); + + $response = $app->handle(Request::create('/foo')); + + $this->assertEquals('Hello world', $response->getContent()); + } + + public function testViewListenerWithCallableTypeHint() + { + $app = new Application(); + $app->get('/foo', function () { return function () { return 'world'; }; }); + $app->view(function (callable $view) { + return new Response('Hello '.$view()); + }); + + $response = $app->handle(Request::create('/foo')); + + $this->assertEquals('Hello world', $response->getContent()); + } + + public function testViewListenersCanBeChained() + { + $app = new Application(); + $app->get('/foo', function () { return (object) array('name' => 'world'); }); + + $app->view(function (\stdClass $view) { + return array('msg' => 'Hello '.$view->name); + }); + + $app->view(function (array $view) { + return $view['msg']; + }); + + $response = $app->handle(Request::create('/foo')); + + $this->assertEquals('Hello world', $response->getContent()); + } + + public function testViewListenersAreIgnoredIfNotSuitable() + { + $app = new Application(); + $app->get('/foo', function () { return 'Hello world'; }); + + $app->view(function (\stdClass $view) { + throw new \Exception('View listener was called'); + }); + + $app->view(function (array $view) { + throw new \Exception('View listener was called'); + }); + + $response = $app->handle(Request::create('/foo')); + + $this->assertEquals('Hello world', $response->getContent()); + } + + public function testViewListenersResponsesAreNotUsedIfNull() + { + $app = new Application(); + $app->get('/foo', function () { return 'Hello world'; }); + + $app->view(function ($view) { + return 'Hello view listener'; + }); + + $app->view(function ($view) { + return; + }); + + $response = $app->handle(Request::create('/foo')); + + $this->assertEquals('Hello view listener', $response->getContent()); + } + + public function testDefaultRoutesFactory() + { + $app = new Application(); + $this->assertInstanceOf('Symfony\Component\Routing\RouteCollection', $app['routes']); + } + + public function testOverriddenRoutesFactory() + { + $app = new Application(); + $app['routes_factory'] = $app->factory(function () { + return new RouteCollectionSubClass(); + }); + $this->assertInstanceOf('Silex\Tests\RouteCollectionSubClass', $app['routes']); + } +} + +class FooController +{ + public function barAction(Application $app, $name) + { + return 'Hello '.$app->escape($name); + } + + public function barSpecialAction(SpecialApplication $app, $name) + { + return 'Hello '.$app->escape($name).' in '.get_class($app); + } +} + +class IncorrectControllerCollection implements ControllerProviderInterface +{ + public function connect(Application $app) + { + return; + } +} + +class RouteCollectionSubClass extends RouteCollection +{ +} + +class SpecialApplication extends Application +{ +} diff --git a/tests/Silex/Tests/CallbackResolverTest.php b/tests/Silex/Tests/CallbackResolverTest.php new file mode 100644 index 0000000..80503b9 --- /dev/null +++ b/tests/Silex/Tests/CallbackResolverTest.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Tests; + +use Pimple\Container; +use Silex\CallbackResolver; + +class CallbackResolverTest extends \PHPUnit_Framework_Testcase +{ + private $app; + private $resolver; + + public function setup() + { + $this->app = new Container(); + $this->resolver = new CallbackResolver($this->app); + } + + public function testShouldResolveCallback() + { + $callable = function () {}; + $this->app['some_service'] = function () { return new \ArrayObject(); }; + $this->app['callable_service'] = function () use ($callable) { + return $callable; + }; + + $this->assertTrue($this->resolver->isValid('some_service:methodName')); + $this->assertTrue($this->resolver->isValid('callable_service')); + $this->assertEquals( + array($this->app['some_service'], 'append'), + $this->resolver->convertCallback('some_service:append') + ); + $this->assertSame($callable, $this->resolver->convertCallback('callable_service')); + } + + /** + * @dataProvider nonStringsAreNotValidProvider + */ + public function testNonStringsAreNotValid($name) + { + $this->assertFalse($this->resolver->isValid($name)); + } + + public function nonStringsAreNotValidProvider() + { + return array( + array(null), + array('some_service::methodName'), + array('missing_service'), + ); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageRegExp /Service "[a-z_]+" is not callable./ + * @dataProvider shouldThrowAnExceptionIfServiceIsNotCallableProvider + */ + public function testShouldThrowAnExceptionIfServiceIsNotCallable($name) + { + $this->app['non_callable_obj'] = function () { return new \stdClass(); }; + $this->app['non_callable'] = function () { return array(); }; + $this->resolver->convertCallback($name); + } + + public function shouldThrowAnExceptionIfServiceIsNotCallableProvider() + { + return array( + array('non_callable_obj:methodA'), + array('non_callable'), + ); + } +} diff --git a/tests/Silex/Tests/CallbackServicesTest.php b/tests/Silex/Tests/CallbackServicesTest.php new file mode 100644 index 0000000..fe96317 --- /dev/null +++ b/tests/Silex/Tests/CallbackServicesTest.php @@ -0,0 +1,109 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests; + +use Silex\Application; +use Symfony\Component\HttpFoundation\Request; +use Silex\Provider\ServiceControllerServiceProvider; + +/** + * Callback as services test cases. + * + * @author Fabien Potencier + */ +class CallbackServicesTest extends \PHPUnit_Framework_TestCase +{ + public $called = array(); + + public function testCallbacksAsServices() + { + $app = new Application(); + $app->register(new ServiceControllerServiceProvider()); + + $app['service'] = function () { + return new CallbackServicesTest(); + }; + + $app->before('service:beforeApp'); + $app->after('service:afterApp'); + $app->finish('service:finishApp'); + $app->error('service:error'); + $app->on('kernel.request', 'service:onRequest'); + + $app + ->match('/', 'service:controller') + ->convert('foo', 'service:convert') + ->before('service:before') + ->after('service:after') + ; + + $request = Request::create('/'); + $response = $app->handle($request); + $app->terminate($request, $response); + + $this->assertEquals(array( + 'BEFORE APP', + 'ON REQUEST', + 'BEFORE', + 'CONVERT', + 'ERROR', + 'AFTER', + 'AFTER APP', + 'FINISH APP', + ), $app['service']->called); + } + + public function controller(Application $app) + { + $app->abort(404); + } + + public function before() + { + $this->called[] = 'BEFORE'; + } + + public function after() + { + $this->called[] = 'AFTER'; + } + + public function beforeApp() + { + $this->called[] = 'BEFORE APP'; + } + + public function afterApp() + { + $this->called[] = 'AFTER APP'; + } + + public function finishApp() + { + $this->called[] = 'FINISH APP'; + } + + public function error() + { + $this->called[] = 'ERROR'; + } + + public function convert() + { + $this->called[] = 'CONVERT'; + } + + public function onRequest() + { + $this->called[] = 'ON REQUEST'; + } +} diff --git a/tests/Silex/Tests/ControllerCollectionTest.php b/tests/Silex/Tests/ControllerCollectionTest.php new file mode 100644 index 0000000..d5c9889 --- /dev/null +++ b/tests/Silex/Tests/ControllerCollectionTest.php @@ -0,0 +1,327 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests; + +use Silex\Application; +use Silex\Controller; +use Silex\ControllerCollection; +use Silex\Exception\ControllerFrozenException; +use Silex\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * ControllerCollection test cases. + * + * @author Igor Wiedler + */ +class ControllerCollectionTest extends \PHPUnit_Framework_TestCase +{ + public function testGetRouteCollectionWithNoRoutes() + { + $controllers = new ControllerCollection(new Route()); + $routes = $controllers->flush(); + $this->assertEquals(0, count($routes->all())); + } + + public function testGetRouteCollectionWithRoutes() + { + $controllers = new ControllerCollection(new Route()); + $controllers->match('/foo', function () {}); + $controllers->match('/bar', function () {}); + + $routes = $controllers->flush(); + $this->assertEquals(2, count($routes->all())); + } + + public function testControllerFreezing() + { + $controllers = new ControllerCollection(new Route()); + + $fooController = $controllers->match('/foo', function () {})->bind('foo'); + $barController = $controllers->match('/bar', function () {})->bind('bar'); + + $controllers->flush(); + + try { + $fooController->bind('foo2'); + $this->fail(); + } catch (ControllerFrozenException $e) { + } + + try { + $barController->bind('bar2'); + $this->fail(); + } catch (ControllerFrozenException $e) { + } + } + + public function testConflictingRouteNames() + { + $controllers = new ControllerCollection(new Route()); + + $mountedRootController = $controllers->match('/', function () {}); + + $mainRootController = new Controller(new Route('/')); + $mainRootController->bind($mainRootController->generateRouteName('main_1')); + + $controllers->flush(); + + $this->assertNotEquals($mainRootController->getRouteName(), $mountedRootController->getRouteName()); + } + + public function testUniqueGeneratedRouteNames() + { + $controllers = new ControllerCollection(new Route()); + + $controllers->match('/a-a', function () {}); + $controllers->match('/a_a', function () {}); + $controllers->match('/a/a', function () {}); + + $routes = $controllers->flush(); + + $this->assertCount(3, $routes->all()); + $this->assertEquals(array('_a_a', '_a_a_1', '_a_a_2'), array_keys($routes->all())); + } + + public function testUniqueGeneratedRouteNamesAmongMounts() + { + $controllers = new ControllerCollection(new Route()); + + $controllers->mount('/root-a', $rootA = new ControllerCollection(new Route())); + $controllers->mount('/root_a', $rootB = new ControllerCollection(new Route())); + + $rootA->match('/leaf', function () {}); + $rootB->match('/leaf', function () {}); + + $routes = $controllers->flush(); + + $this->assertCount(2, $routes->all()); + $this->assertEquals(array('_root_a_leaf', '_root_a_leaf_1'), array_keys($routes->all())); + } + + public function testUniqueGeneratedRouteNamesAmongNestedMounts() + { + $controllers = new ControllerCollection(new Route()); + + $controllers->mount('/root-a', $rootA = new ControllerCollection(new Route())); + $controllers->mount('/root_a', $rootB = new ControllerCollection(new Route())); + + $rootA->mount('/tree', $treeA = new ControllerCollection(new Route())); + $rootB->mount('/tree', $treeB = new ControllerCollection(new Route())); + + $treeA->match('/leaf', function () {}); + $treeB->match('/leaf', function () {}); + + $routes = $controllers->flush(); + + $this->assertCount(2, $routes->all()); + $this->assertEquals(array('_root_a_tree_leaf', '_root_a_tree_leaf_1'), array_keys($routes->all())); + } + + public function testMountCallable() + { + $controllers = new ControllerCollection(new Route()); + $controllers->mount('/prefix', function (ControllerCollection $coll) { + $coll->mount('/path', function ($coll) { + $coll->get('/part'); + }); + }); + + $routes = $controllers->flush(); + $this->assertEquals('/prefix/path/part', current($routes->all())->getPath()); + } + + public function testMountCallableProperClone() + { + $controllers = new ControllerCollection(new Route(), new RouteCollection()); + $controllers->get('/'); + + $subControllers = null; + $controllers->mount('/prefix', function (ControllerCollection $coll) use (&$subControllers) { + $subControllers = $coll; + $coll->get('/'); + }); + + $routes = $controllers->flush(); + $subRoutes = $subControllers->flush(); + $this->assertTrue($routes->count() == 2 && $subRoutes->count() == 0); + } + + public function testMountControllersFactory() + { + $testControllers = new ControllerCollection(new Route()); + $controllers = new ControllerCollection(new Route(), null, function () use ($testControllers) { + return $testControllers; + }); + + $controllers->mount('/prefix', function ($mounted) use ($testControllers) { + $this->assertSame($mounted, $testControllers); + }); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage The "mount" method takes either a "ControllerCollection" instance or callable. + */ + public function testMountCallableException() + { + $controllers = new ControllerCollection(new Route()); + $controllers->mount('/prefix', ''); + } + + public function testAssert() + { + $controllers = new ControllerCollection(new Route()); + $controllers->assert('id', '\d+'); + $controller = $controllers->match('/{id}/{name}/{extra}', function () {})->assert('name', '\w+')->assert('extra', '.*'); + $controllers->assert('extra', '\w+'); + + $this->assertEquals('\d+', $controller->getRoute()->getRequirement('id')); + $this->assertEquals('\w+', $controller->getRoute()->getRequirement('name')); + $this->assertEquals('\w+', $controller->getRoute()->getRequirement('extra')); + } + + public function testValue() + { + $controllers = new ControllerCollection(new Route()); + $controllers->value('id', '1'); + $controller = $controllers->match('/{id}/{name}/{extra}', function () {})->value('name', 'Fabien')->value('extra', 'Symfony'); + $controllers->value('extra', 'Twig'); + + $this->assertEquals('1', $controller->getRoute()->getDefault('id')); + $this->assertEquals('Fabien', $controller->getRoute()->getDefault('name')); + $this->assertEquals('Twig', $controller->getRoute()->getDefault('extra')); + } + + public function testConvert() + { + $controllers = new ControllerCollection(new Route()); + $controllers->convert('id', '1'); + $controller = $controllers->match('/{id}/{name}/{extra}', function () {})->convert('name', 'Fabien')->convert('extra', 'Symfony'); + $controllers->convert('extra', 'Twig'); + + $this->assertEquals(array('id' => '1', 'name' => 'Fabien', 'extra' => 'Twig'), $controller->getRoute()->getOption('_converters')); + } + + public function testRequireHttp() + { + $controllers = new ControllerCollection(new Route()); + $controllers->requireHttp(); + $controller = $controllers->match('/{id}/{name}/{extra}', function () {})->requireHttps(); + + $this->assertEquals(array('https'), $controller->getRoute()->getSchemes()); + + $controllers->requireHttp(); + + $this->assertEquals(array('http'), $controller->getRoute()->getSchemes()); + } + + public function testBefore() + { + $controllers = new ControllerCollection(new Route()); + $controllers->before('mid1'); + $controller = $controllers->match('/{id}/{name}/{extra}', function () {})->before('mid2'); + $controllers->before('mid3'); + + $this->assertEquals(array('mid1', 'mid2', 'mid3'), $controller->getRoute()->getOption('_before_middlewares')); + } + + public function testAfter() + { + $controllers = new ControllerCollection(new Route()); + $controllers->after('mid1'); + $controller = $controllers->match('/{id}/{name}/{extra}', function () {})->after('mid2'); + $controllers->after('mid3'); + + $this->assertEquals(array('mid1', 'mid2', 'mid3'), $controller->getRoute()->getOption('_after_middlewares')); + } + + public function testWhen() + { + $controllers = new ControllerCollection(new Route()); + $controller = $controllers->match('/{id}/{name}/{extra}', function () {})->when('request.isSecure() == true'); + + $this->assertEquals('request.isSecure() == true', $controller->getRoute()->getCondition()); + } + + public function testRouteExtension() + { + $route = new MyRoute1(); + + $controller = new ControllerCollection($route); + $controller->foo('foo'); + + $this->assertEquals('foo', $route->foo); + } + + /** + * @expectedException \BadMethodCallException + */ + public function testRouteMethodDoesNotExist() + { + $route = new MyRoute1(); + + $controller = new ControllerCollection($route); + $controller->bar(); + } + + public function testNestedCollectionRouteCallbacks() + { + $cl1 = new ControllerCollection(new MyRoute1()); + $cl2 = new ControllerCollection(new MyRoute1()); + + $c1 = $cl2->match('/c1', function () {}); + $cl1->mount('/foo', $cl2); + $c2 = $cl2->match('/c2', function () {}); + $cl1->before('before'); + $c3 = $cl2->match('/c3', function () {}); + + $cl1->flush(); + + $this->assertEquals(array('before'), $c1->getRoute()->getOption('_before_middlewares')); + $this->assertEquals(array('before'), $c2->getRoute()->getOption('_before_middlewares')); + $this->assertEquals(array('before'), $c3->getRoute()->getOption('_before_middlewares')); + } + + public function testRoutesFactoryOmitted() + { + $controllers = new ControllerCollection(new Route()); + $routes = $controllers->flush(); + $this->assertInstanceOf('Symfony\Component\Routing\RouteCollection', $routes); + } + + public function testRoutesFactoryInConstructor() + { + $app = new Application(); + $app['routes_factory'] = $app->factory(function () { + return new RouteCollectionSubClass2(); + }); + + $controllers = new ControllerCollection(new Route(), $app['routes_factory']); + $routes = $controllers->flush(); + $this->assertInstanceOf('Silex\Tests\RouteCollectionSubClass2', $routes); + } +} + +class MyRoute1 extends Route +{ + public $foo; + + public function foo($value) + { + $this->foo = $value; + } +} + +class RouteCollectionSubClass2 extends RouteCollection +{ +} diff --git a/tests/Silex/Tests/ControllerResolverTest.php b/tests/Silex/Tests/ControllerResolverTest.php new file mode 100644 index 0000000..e448388 --- /dev/null +++ b/tests/Silex/Tests/ControllerResolverTest.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests; + +use Silex\ControllerResolver; +use Silex\Application; +use Symfony\Component\HttpFoundation\Request; + +/** + * ControllerResolver test cases. + * + * @author Fabien Potencier + */ +class ControllerResolverTest extends \PHPUnit_Framework_TestCase +{ + /** + * @group legacy + */ + public function testGetArguments() + { + $app = new Application(); + $resolver = new ControllerResolver($app); + + $controller = function (Application $app) {}; + + $args = $resolver->getArguments(Request::create('/'), $controller); + $this->assertSame($app, $args[0]); + } +} diff --git a/tests/Silex/Tests/ControllerTest.php b/tests/Silex/Tests/ControllerTest.php new file mode 100644 index 0000000..791563f --- /dev/null +++ b/tests/Silex/Tests/ControllerTest.php @@ -0,0 +1,132 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests; + +use Silex\Controller; +use Silex\Route; + +/** + * Controller test cases. + * + * @author Igor Wiedler + */ +class ControllerTest extends \PHPUnit_Framework_TestCase +{ + public function testBind() + { + $controller = new Controller(new Route('/foo')); + $ret = $controller->bind('foo'); + + $this->assertSame($ret, $controller); + $this->assertEquals('foo', $controller->getRouteName()); + } + + /** + * @expectedException \Silex\Exception\ControllerFrozenException + */ + public function testBindOnFrozenControllerShouldThrowException() + { + $controller = new Controller(new Route('/foo')); + $controller->bind('foo'); + $controller->freeze(); + $controller->bind('bar'); + } + + public function testAssert() + { + $controller = new Controller(new Route('/foo/{bar}')); + $ret = $controller->assert('bar', '\d+'); + + $this->assertSame($ret, $controller); + $this->assertEquals(array('bar' => '\d+'), $controller->getRoute()->getRequirements()); + } + + public function testValue() + { + $controller = new Controller(new Route('/foo/{bar}')); + $ret = $controller->value('bar', 'foo'); + + $this->assertSame($ret, $controller); + $this->assertEquals(array('bar' => 'foo'), $controller->getRoute()->getDefaults()); + } + + public function testConvert() + { + $controller = new Controller(new Route('/foo/{bar}')); + $ret = $controller->convert('bar', $func = function ($bar) { return $bar; }); + + $this->assertSame($ret, $controller); + $this->assertEquals(array('bar' => $func), $controller->getRoute()->getOption('_converters')); + } + + public function testRun() + { + $controller = new Controller(new Route('/foo/{bar}')); + $ret = $controller->run($cb = function () { return 'foo'; }); + + $this->assertSame($ret, $controller); + $this->assertEquals($cb, $controller->getRoute()->getDefault('_controller')); + } + + /** + * @dataProvider provideRouteAndExpectedRouteName + */ + public function testDefaultRouteNameGeneration(Route $route, $prefix, $expectedRouteName) + { + $controller = new Controller($route); + $controller->bind($controller->generateRouteName($prefix)); + + $this->assertEquals($expectedRouteName, $controller->getRouteName()); + } + + public function provideRouteAndExpectedRouteName() + { + return array( + array(new Route('/Invalid%Symbols#Stripped', array(), array(), array(), '', array(), array('POST')), '', 'POST_InvalidSymbolsStripped'), + array(new Route('/post/{id}', array(), array(), array(), '', array(), array('GET')), '', 'GET_post_id'), + array(new Route('/colon:pipe|dashes-escaped'), '', '_colon_pipe_dashes_escaped'), + array(new Route('/underscores_and.periods'), '', '_underscores_and.periods'), + array(new Route('/post/{id}', array(), array(), array(), '', array(), array('GET')), 'prefix', 'GET_prefix_post_id'), + ); + } + + public function testRouteExtension() + { + $route = new MyRoute(); + + $controller = new Controller($route); + $controller->foo('foo'); + + $this->assertEquals('foo', $route->foo); + } + + /** + * @expectedException \BadMethodCallException + */ + public function testRouteMethodDoesNotExist() + { + $route = new MyRoute(); + + $controller = new Controller($route); + $controller->bar(); + } +} + +class MyRoute extends Route +{ + public $foo; + + public function foo($value) + { + $this->foo = $value; + } +} diff --git a/tests/Silex/Tests/EventListener/LogListenerTest.php b/tests/Silex/Tests/EventListener/LogListenerTest.php new file mode 100644 index 0000000..9deae8c --- /dev/null +++ b/tests/Silex/Tests/EventListener/LogListenerTest.php @@ -0,0 +1,93 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\EventListener; + +use Psr\Log\LogLevel; +use Silex\EventListener\LogListener; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\Exception\HttpException; + +/** + * LogListener. + * + * @author Jérôme Tamarelle + */ +class LogListenerTest extends \PHPUnit_Framework_TestCase +{ + public function testRequestListener() + { + $logger = $this->getMock('Psr\\Log\\LoggerInterface'); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::DEBUG, '> GET /foo') + ; + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber(new LogListener($logger)); + + $kernel = $this->getMock('Symfony\\Component\\HttpKernel\\HttpKernelInterface'); + + $dispatcher->dispatch(KernelEvents::REQUEST, new GetResponseEvent($kernel, Request::create('/subrequest'), HttpKernelInterface::SUB_REQUEST), 'Skip sub requests'); + + $dispatcher->dispatch(KernelEvents::REQUEST, new GetResponseEvent($kernel, Request::create('/foo'), HttpKernelInterface::MASTER_REQUEST), 'Log master requests'); + } + + public function testResponseListener() + { + $logger = $this->getMock('Psr\\Log\\LoggerInterface'); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::DEBUG, '< 301') + ; + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber(new LogListener($logger)); + + $kernel = $this->getMock('Symfony\\Component\\HttpKernel\\HttpKernelInterface'); + + $dispatcher->dispatch(KernelEvents::RESPONSE, new FilterResponseEvent($kernel, Request::create('/foo'), HttpKernelInterface::SUB_REQUEST, Response::create('subrequest', 200)), 'Skip sub requests'); + + $dispatcher->dispatch(KernelEvents::RESPONSE, new FilterResponseEvent($kernel, Request::create('/foo'), HttpKernelInterface::MASTER_REQUEST, Response::create('bar', 301)), 'Log master requests'); + } + + public function testExceptionListener() + { + $logger = $this->getMock('Psr\\Log\\LoggerInterface'); + $logger + ->expects($this->at(0)) + ->method('log') + ->with(LogLevel::CRITICAL, 'RuntimeException: Fatal error (uncaught exception) at '.__FILE__.' line '.(__LINE__ + 13)) + ; + $logger + ->expects($this->at(1)) + ->method('log') + ->with(LogLevel::ERROR, 'Symfony\Component\HttpKernel\Exception\HttpException: Http error (uncaught exception) at '.__FILE__.' line '.(__LINE__ + 9)) + ; + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber(new LogListener($logger)); + + $kernel = $this->getMock('Symfony\\Component\\HttpKernel\\HttpKernelInterface'); + + $dispatcher->dispatch(KernelEvents::EXCEPTION, new GetResponseForExceptionEvent($kernel, Request::create('/foo'), HttpKernelInterface::SUB_REQUEST, new \RuntimeException('Fatal error'))); + $dispatcher->dispatch(KernelEvents::EXCEPTION, new GetResponseForExceptionEvent($kernel, Request::create('/foo'), HttpKernelInterface::SUB_REQUEST, new HttpException(400, 'Http error'))); + } +} diff --git a/tests/Silex/Tests/ExceptionHandlerTest.php b/tests/Silex/Tests/ExceptionHandlerTest.php new file mode 100644 index 0000000..24e9a0d --- /dev/null +++ b/tests/Silex/Tests/ExceptionHandlerTest.php @@ -0,0 +1,406 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests; + +use Silex\Application; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +/** + * Error handler test cases. + * + * @author Igor Wiedler + */ +class ExceptionHandlerTest extends \PHPUnit_Framework_TestCase +{ + public function testExceptionHandlerExceptionNoDebug() + { + $app = new Application(); + $app['debug'] = false; + + $app->match('/foo', function () { + throw new \RuntimeException('foo exception'); + }); + + $request = Request::create('/foo'); + $response = $app->handle($request); + $this->assertContains('

Whoops, looks like something went wrong.

', $response->getContent()); + $this->assertEquals(500, $response->getStatusCode()); + } + + public function testExceptionHandlerExceptionDebug() + { + $app = new Application(); + $app['debug'] = true; + + $app->match('/foo', function () { + throw new \RuntimeException('foo exception'); + }); + + $request = Request::create('/foo'); + $response = $app->handle($request); + + $this->assertContains('foo exception', $response->getContent()); + $this->assertEquals(500, $response->getStatusCode()); + } + + public function testExceptionHandlerNotFoundNoDebug() + { + $app = new Application(); + $app['debug'] = false; + + $request = Request::create('/foo'); + $response = $app->handle($request); + $this->assertContains('

Sorry, the page you are looking for could not be found.

', $response->getContent()); + $this->assertEquals(404, $response->getStatusCode()); + } + + public function testExceptionHandlerNotFoundDebug() + { + $app = new Application(); + $app['debug'] = true; + + $request = Request::create('/foo'); + $response = $app->handle($request); + $this->assertContains('No route found for "GET /foo"', html_entity_decode($response->getContent())); + $this->assertEquals(404, $response->getStatusCode()); + } + + public function testExceptionHandlerMethodNotAllowedNoDebug() + { + $app = new Application(); + $app['debug'] = false; + + $app->get('/foo', function () { return 'foo'; }); + + $request = Request::create('/foo', 'POST'); + $response = $app->handle($request); + $this->assertContains('

Whoops, looks like something went wrong.

', $response->getContent()); + $this->assertEquals(405, $response->getStatusCode()); + $this->assertEquals('GET', $response->headers->get('Allow')); + } + + public function testExceptionHandlerMethodNotAllowedDebug() + { + $app = new Application(); + $app['debug'] = true; + + $app->get('/foo', function () { return 'foo'; }); + + $request = Request::create('/foo', 'POST'); + $response = $app->handle($request); + $this->assertContains('No route found for "POST /foo": Method Not Allowed (Allow: GET)', html_entity_decode($response->getContent())); + $this->assertEquals(405, $response->getStatusCode()); + $this->assertEquals('GET', $response->headers->get('Allow')); + } + + public function testNoExceptionHandler() + { + $app = new Application(); + unset($app['exception_handler']); + + $app->match('/foo', function () { + throw new \RuntimeException('foo exception'); + }); + + try { + $request = Request::create('/foo'); + $app->handle($request); + $this->fail('->handle() should not catch exceptions where no error handler was supplied'); + } catch (\RuntimeException $e) { + $this->assertEquals('foo exception', $e->getMessage()); + } + } + + public function testOneExceptionHandler() + { + $app = new Application(); + + $app->match('/500', function () { + throw new \RuntimeException('foo exception'); + }); + + $app->match('/404', function () { + throw new NotFoundHttpException('foo exception'); + }); + + $app->get('/405', function () { return 'foo'; }); + + $app->error(function ($e, $code) { + return new Response('foo exception handler'); + }); + + $response = $this->checkRouteResponse($app, '/500', 'foo exception handler'); + $this->assertEquals(500, $response->getStatusCode()); + + $response = $app->handle(Request::create('/404')); + $this->assertEquals(404, $response->getStatusCode()); + + $response = $app->handle(Request::create('/405', 'POST')); + $this->assertEquals(405, $response->getStatusCode()); + $this->assertEquals('GET', $response->headers->get('Allow')); + } + + public function testMultipleExceptionHandlers() + { + $app = new Application(); + + $app->match('/foo', function () { + throw new \RuntimeException('foo exception'); + }); + + $errors = 0; + + $app->error(function ($e) use (&$errors) { + ++$errors; + }); + + $app->error(function ($e) use (&$errors) { + ++$errors; + }); + + $app->error(function ($e) use (&$errors) { + ++$errors; + + return new Response('foo exception handler'); + }); + + $app->error(function ($e) use (&$errors) { + // should not execute + ++$errors; + }); + + $request = Request::create('/foo'); + $this->checkRouteResponse($app, '/foo', 'foo exception handler', 'should return the first response returned by an exception handler'); + + $this->assertEquals(3, $errors, 'should execute error handlers until a response is returned'); + } + + public function testNoResponseExceptionHandler() + { + $app = new Application(); + unset($app['exception_handler']); + + $app->match('/foo', function () { + throw new \RuntimeException('foo exception'); + }); + + $errors = 0; + + $app->error(function ($e) use (&$errors) { + ++$errors; + }); + + try { + $request = Request::create('/foo'); + $app->handle($request); + $this->fail('->handle() should not catch exceptions where an empty error handler was supplied'); + } catch (\RuntimeException $e) { + $this->assertEquals('foo exception', $e->getMessage()); + } catch (\LogicException $e) { + $this->assertEquals('foo exception', $e->getPrevious()->getMessage()); + } + + $this->assertEquals(1, $errors, 'should execute the error handler'); + } + + public function testStringResponseExceptionHandler() + { + $app = new Application(); + + $app->match('/foo', function () { + throw new \RuntimeException('foo exception'); + }); + + $app->error(function ($e) { + return 'foo exception handler'; + }); + + $request = Request::create('/foo'); + $this->checkRouteResponse($app, '/foo', 'foo exception handler', 'should accept a string response from the error handler'); + } + + public function testExceptionHandlerException() + { + $app = new Application(); + + $app->match('/foo', function () { + throw new \RuntimeException('foo exception'); + }); + + $app->error(function ($e) { + throw new \RuntimeException('foo exception handler exception'); + }); + + try { + $request = Request::create('/foo'); + $app->handle($request); + $this->fail('->handle() should not catch exceptions thrown from an error handler'); + } catch (\RuntimeException $e) { + $this->assertEquals('foo exception handler exception', $e->getMessage()); + } + } + + public function testRemoveExceptionHandlerAfterDispatcherAccess() + { + $app = new Application(); + + $app->match('/foo', function () { + throw new \RuntimeException('foo exception'); + }); + + $app->before(function () { + // just making sure the dispatcher gets created + }); + + unset($app['exception_handler']); + + try { + $request = Request::create('/foo'); + $app->handle($request); + $this->fail('default exception handler should have been removed'); + } catch (\RuntimeException $e) { + $this->assertEquals('foo exception', $e->getMessage()); + } + } + + public function testExceptionHandlerWithDefaultException() + { + $app = new Application(); + $app['debug'] = false; + + $app->match('/foo', function () { + throw new \Exception(); + }); + + $app->error(function (\Exception $e) { + return new Response('Exception thrown', 500); + }); + + $request = Request::create('/foo'); + $response = $app->handle($request); + $this->assertContains('Exception thrown', $response->getContent()); + $this->assertEquals(500, $response->getStatusCode()); + } + + public function testExceptionHandlerWithStandardException() + { + $app = new Application(); + $app['debug'] = false; + + $app->match('/foo', function () { + // Throw a normal exception + throw new \Exception(); + }); + + // Register 2 error handlers, each with a specified Exception class + // Since we throw a standard Exception above only + // the second error handler should fire + $app->error(function (\LogicException $e) { // Extends \Exception + + return 'Caught LogicException'; + }); + $app->error(function (\Exception $e) { + return 'Caught Exception'; + }); + + $request = Request::create('/foo'); + $response = $app->handle($request); + $this->assertContains('Caught Exception', $response->getContent()); + } + + public function testExceptionHandlerWithSpecifiedException() + { + $app = new Application(); + $app['debug'] = false; + + $app->match('/foo', function () { + // Throw a specified exception + throw new \LogicException(); + }); + + // Register 2 error handlers, each with a specified Exception class + // Since we throw a LogicException above + // the first error handler should fire + $app->error(function (\LogicException $e) { // Extends \Exception + + return 'Caught LogicException'; + }); + $app->error(function (\Exception $e) { + return 'Caught Exception'; + }); + + $request = Request::create('/foo'); + $response = $app->handle($request); + $this->assertContains('Caught LogicException', $response->getContent()); + } + + public function testExceptionHandlerWithSpecifiedExceptionInReverseOrder() + { + $app = new Application(); + $app['debug'] = false; + + $app->match('/foo', function () { + // Throw a specified exception + throw new \LogicException(); + }); + + // Register the \Exception error handler first, since the + // error handler works with an instanceof mechanism the + // second more specific error handler should not fire since + // the \Exception error handler is registered first and also + // captures all exceptions that extend it + $app->error(function (\Exception $e) { + return 'Caught Exception'; + }); + $app->error(function (\LogicException $e) { // Extends \Exception + + return 'Caught LogicException'; + }); + + $request = Request::create('/foo'); + $response = $app->handle($request); + $this->assertContains('Caught Exception', $response->getContent()); + } + + public function testExceptionHandlerWithArrayStyleCallback() + { + $app = new Application(); + $app['debug'] = false; + + $app->match('/foo', function () { + throw new \Exception(); + }); + + // Array style callback for error handler + $app->error(array($this, 'exceptionHandler')); + + $request = Request::create('/foo'); + $response = $app->handle($request); + $this->assertContains('Caught Exception', $response->getContent()); + } + + protected function checkRouteResponse($app, $path, $expectedContent, $method = 'get', $message = null) + { + $request = Request::create($path, $method); + $response = $app->handle($request); + $this->assertEquals($expectedContent, $response->getContent(), $message); + + return $response; + } + + public function exceptionHandler() + { + return 'Caught Exception'; + } +} diff --git a/tests/Silex/Tests/FunctionalTest.php b/tests/Silex/Tests/FunctionalTest.php new file mode 100644 index 0000000..f2af4ac --- /dev/null +++ b/tests/Silex/Tests/FunctionalTest.php @@ -0,0 +1,58 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests; + +use Silex\Application; +use Silex\Route; +use Silex\ControllerCollection; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Functional test cases. + * + * @author Igor Wiedler + */ +class FunctionalTest extends \PHPUnit_Framework_TestCase +{ + public function testBind() + { + $app = new Application(); + + $app->get('/', function () { + return 'hello'; + }) + ->bind('homepage'); + + $app->get('/foo', function () { + return 'foo'; + }) + ->bind('foo_abc'); + + $app->flush(); + $routes = $app['routes']; + $this->assertInstanceOf('Symfony\Component\Routing\Route', $routes->get('homepage')); + $this->assertInstanceOf('Symfony\Component\Routing\Route', $routes->get('foo_abc')); + } + + public function testMount() + { + $mounted = new ControllerCollection(new Route()); + $mounted->get('/{name}', function ($name) { return new Response($name); }); + + $app = new Application(); + $app->mount('/hello', $mounted); + + $response = $app->handle(Request::create('/hello/Silex')); + $this->assertEquals('Silex', $response->getContent()); + } +} diff --git a/tests/Silex/Tests/JsonTest.php b/tests/Silex/Tests/JsonTest.php new file mode 100644 index 0000000..5eb1336 --- /dev/null +++ b/tests/Silex/Tests/JsonTest.php @@ -0,0 +1,56 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests; + +use Silex\Application; + +/** + * JSON test cases. + * + * @author Igor Wiedler + */ +class JsonTest extends \PHPUnit_Framework_TestCase +{ + public function testJsonReturnsJsonResponse() + { + $app = new Application(); + + $response = $app->json(); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\JsonResponse', $response); + $response = json_decode($response->getContent(), true); + $this->assertSame(array(), $response); + } + + public function testJsonUsesData() + { + $app = new Application(); + + $response = $app->json(array('foo' => 'bar')); + $this->assertSame('{"foo":"bar"}', $response->getContent()); + } + + public function testJsonUsesStatus() + { + $app = new Application(); + + $response = $app->json(array(), 202); + $this->assertSame(202, $response->getStatusCode()); + } + + public function testJsonUsesHeaders() + { + $app = new Application(); + + $response = $app->json(array(), 200, array('ETag' => 'foo')); + $this->assertSame('foo', $response->headers->get('ETag')); + } +} diff --git a/tests/Silex/Tests/LazyDispatcherTest.php b/tests/Silex/Tests/LazyDispatcherTest.php new file mode 100644 index 0000000..1b4c580 --- /dev/null +++ b/tests/Silex/Tests/LazyDispatcherTest.php @@ -0,0 +1,59 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests; + +use Silex\Application; +use Symfony\Component\HttpFoundation\Request; + +class LazyDispatcherTest extends \PHPUnit_Framework_TestCase +{ + /** @test */ + public function beforeMiddlewareShouldNotCreateDispatcherEarly() + { + $dispatcherCreated = false; + + $app = new Application(); + $app->extend('dispatcher', function ($dispatcher, $app) use (&$dispatcherCreated) { + $dispatcherCreated = true; + + return $dispatcher; + }); + + $app->before(function () {}); + + $this->assertFalse($dispatcherCreated); + + $request = Request::create('/'); + $app->handle($request); + + $this->assertTrue($dispatcherCreated); + } + + /** @test */ + public function eventHelpersShouldDirectlyAddListenersAfterBoot() + { + $app = new Application(); + + $fired = false; + $app->get('/', function () use ($app, &$fired) { + $app->finish(function () use (&$fired) { + $fired = true; + }); + }); + + $request = Request::create('/'); + $response = $app->handle($request); + $app->terminate($request, $response); + + $this->assertTrue($fired, 'Event was not fired'); + } +} diff --git a/tests/Silex/Tests/LazyRequestMatcherTest.php b/tests/Silex/Tests/LazyRequestMatcherTest.php new file mode 100644 index 0000000..d81eb56 --- /dev/null +++ b/tests/Silex/Tests/LazyRequestMatcherTest.php @@ -0,0 +1,77 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests; + +use Symfony\Component\HttpFoundation\Request; +use Silex\Provider\Routing\LazyRequestMatcher; + +/** + * LazyRequestMatcher test case. + * + * @author Leszek Prabucki + */ +class LazyRequestMatcherTest extends \PHPUnit_Framework_TestCase +{ + /** + * @covers Silex\LazyRequestMatcher::getRequestMatcher + */ + public function testUserMatcherIsCreatedLazily() + { + $callCounter = 0; + $requestMatcher = $this->getMock('Symfony\Component\Routing\Matcher\RequestMatcherInterface'); + + $matcher = new LazyRequestMatcher(function () use ($requestMatcher, &$callCounter) { + ++$callCounter; + + return $requestMatcher; + }); + + $this->assertEquals(0, $callCounter); + $request = Request::create('path'); + $matcher->matchRequest($request); + $this->assertEquals(1, $callCounter); + } + + /** + * @expectedException LogicException + * @expectedExceptionMessage Factory supplied to LazyRequestMatcher must return implementation of Symfony\Component\Routing\RequestMatcherInterface. + */ + public function testThatCanInjectRequestMatcherOnly() + { + $matcher = new LazyRequestMatcher(function () { + return 'someMatcher'; + }); + + $request = Request::create('path'); + $matcher->matchRequest($request); + } + + /** + * @covers Silex\LazyRequestMatcher::matchRequest + */ + public function testMatchIsProxy() + { + $request = Request::create('path'); + $matcher = $this->getMock('Symfony\Component\Routing\Matcher\RequestMatcherInterface'); + $matcher->expects($this->once()) + ->method('matchRequest') + ->with($request) + ->will($this->returnValue('matcherReturnValue')); + + $matcher = new LazyRequestMatcher(function () use ($matcher) { + return $matcher; + }); + $result = $matcher->matchRequest($request); + + $this->assertEquals('matcherReturnValue', $result); + } +} diff --git a/tests/Silex/Tests/LocaleTest.php b/tests/Silex/Tests/LocaleTest.php new file mode 100644 index 0000000..ada57be --- /dev/null +++ b/tests/Silex/Tests/LocaleTest.php @@ -0,0 +1,83 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests; + +use Silex\Application; +use Silex\Provider\LocaleServiceProvider; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +/** + * Locale test cases. + * + * @author Fabien Potencier + */ +class LocaleTest extends \PHPUnit_Framework_TestCase +{ + public function testLocale() + { + $app = new Application(); + $app->register(new LocaleServiceProvider()); + $app->get('/', function (Request $request) { return $request->getLocale(); }); + $response = $app->handle(Request::create('/')); + $this->assertEquals('en', $response->getContent()); + + $app = new Application(); + $app->register(new LocaleServiceProvider()); + $app['locale'] = 'fr'; + $app->get('/', function (Request $request) { return $request->getLocale(); }); + $response = $app->handle(Request::create('/')); + $this->assertEquals('fr', $response->getContent()); + + $app = new Application(); + $app->register(new LocaleServiceProvider()); + $app->get('/{_locale}', function (Request $request) { return $request->getLocale(); }); + $response = $app->handle(Request::create('/es')); + $this->assertEquals('es', $response->getContent()); + } + + public function testLocaleInSubRequests() + { + $app = new Application(); + $app->register(new LocaleServiceProvider()); + $app->get('/embed/{_locale}', function (Request $request) { return $request->getLocale(); }); + $app->get('/{_locale}', function (Request $request) use ($app) { + return $request->getLocale().$app->handle(Request::create('/embed/es'), HttpKernelInterface::SUB_REQUEST)->getContent().$request->getLocale(); + }); + $response = $app->handle(Request::create('/fr')); + $this->assertEquals('fresfr', $response->getContent()); + + $app = new Application(); + $app->register(new LocaleServiceProvider()); + $app->get('/embed', function (Request $request) { return $request->getLocale(); }); + $app->get('/{_locale}', function (Request $request) use ($app) { + return $request->getLocale().$app->handle(Request::create('/embed'), HttpKernelInterface::SUB_REQUEST)->getContent().$request->getLocale(); + }); + $response = $app->handle(Request::create('/fr')); + // locale in sub-request must be "en" as this is the value if the sub-request is converted to an ESI + $this->assertEquals('frenfr', $response->getContent()); + } + + public function testLocaleWithBefore() + { + $app = new Application(); + $app->register(new LocaleServiceProvider()); + $app->before(function (Request $request) use ($app) { $request->setLocale('fr'); }); + $app->get('/embed', function (Request $request) { return $request->getLocale(); }); + $app->get('/', function (Request $request) use ($app) { + return $request->getLocale().$app->handle(Request::create('/embed'), HttpKernelInterface::SUB_REQUEST)->getContent().$request->getLocale(); + }); + $response = $app->handle(Request::create('/')); + // locale in sub-request is "en" as the before filter is only executed for the main request + $this->assertEquals('frenfr', $response->getContent()); + } +} diff --git a/tests/Silex/Tests/MiddlewareTest.php b/tests/Silex/Tests/MiddlewareTest.php new file mode 100644 index 0000000..376a42c --- /dev/null +++ b/tests/Silex/Tests/MiddlewareTest.php @@ -0,0 +1,307 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests; + +use Silex\Application; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Middleware test cases. + * + * @author Igor Wiedler + */ +class MiddlewareTest extends \PHPUnit_Framework_TestCase +{ + public function testBeforeAndAfterFilter() + { + $i = 0; + $test = $this; + + $app = new Application(); + + $app->before(function () use (&$i, $test) { + $test->assertEquals(0, $i); + ++$i; + }); + + $app->match('/foo', function () use (&$i, $test) { + $test->assertEquals(1, $i); + ++$i; + }); + + $app->after(function () use (&$i, $test) { + $test->assertEquals(2, $i); + ++$i; + }); + + $request = Request::create('/foo'); + $app->handle($request); + + $this->assertEquals(3, $i); + } + + public function testAfterFilterWithResponseObject() + { + $i = 0; + + $app = new Application(); + + $app->match('/foo', function () use (&$i) { + ++$i; + + return new Response('foo'); + }); + + $app->after(function () use (&$i) { + ++$i; + }); + + $request = Request::create('/foo'); + $app->handle($request); + + $this->assertEquals(2, $i); + } + + public function testMultipleFilters() + { + $i = 0; + $test = $this; + + $app = new Application(); + + $app->before(function () use (&$i, $test) { + $test->assertEquals(0, $i); + ++$i; + }); + + $app->before(function () use (&$i, $test) { + $test->assertEquals(1, $i); + ++$i; + }); + + $app->match('/foo', function () use (&$i, $test) { + $test->assertEquals(2, $i); + ++$i; + }); + + $app->after(function () use (&$i, $test) { + $test->assertEquals(3, $i); + ++$i; + }); + + $app->after(function () use (&$i, $test) { + $test->assertEquals(4, $i); + ++$i; + }); + + $request = Request::create('/foo'); + $app->handle($request); + + $this->assertEquals(5, $i); + } + + public function testFiltersShouldFireOnException() + { + $i = 0; + + $app = new Application(); + + $app->before(function () use (&$i) { + ++$i; + }); + + $app->match('/foo', function () { + throw new \RuntimeException(); + }); + + $app->after(function () use (&$i) { + ++$i; + }); + + $app->error(function () { + return 'error handled'; + }); + + $request = Request::create('/foo'); + $app->handle($request); + + $this->assertEquals(2, $i); + } + + public function testFiltersShouldFireOnHttpException() + { + $i = 0; + + $app = new Application(); + + $app->before(function () use (&$i) { + ++$i; + }, Application::EARLY_EVENT); + + $app->after(function () use (&$i) { + ++$i; + }); + + $app->error(function () { + return 'error handled'; + }); + + $request = Request::create('/nowhere'); + $app->handle($request); + + $this->assertEquals(2, $i); + } + + public function testBeforeFilterPreventsBeforeMiddlewaresToBeExecuted() + { + $app = new Application(); + + $app->before(function () { return new Response('app before'); }); + + $app->get('/', function () { + return new Response('test'); + })->before(function () { + return new Response('middleware before'); + }); + + $this->assertEquals('app before', $app->handle(Request::create('/'))->getContent()); + } + + public function testBeforeFilterExceptionsWhenHandlingAnException() + { + $app = new Application(); + + $app->before(function () { throw new \RuntimeException(''); }); + + // even if the before filter throws an exception, we must have the 404 + $this->assertEquals(404, $app->handle(Request::create('/'))->getStatusCode()); + } + + public function testRequestShouldBePopulatedOnBefore() + { + $app = new Application(); + + $app->before(function (Request $request) use ($app) { + $app['project'] = $request->get('project'); + }); + + $app->match('/foo/{project}', function () use ($app) { + return $app['project']; + }); + + $request = Request::create('/foo/bar'); + $this->assertEquals('bar', $app->handle($request)->getContent()); + + $request = Request::create('/foo/baz'); + $this->assertEquals('baz', $app->handle($request)->getContent()); + } + + public function testBeforeFilterAccessesRequestAndCanReturnResponse() + { + $app = new Application(); + + $app->before(function (Request $request) { + return new Response($request->get('name')); + }); + + $app->match('/', function () use ($app) { throw new \Exception('Should never be executed'); }); + + $request = Request::create('/?name=Fabien'); + $this->assertEquals('Fabien', $app->handle($request)->getContent()); + } + + public function testAfterFilterAccessRequestResponse() + { + $app = new Application(); + + $app->after(function (Request $request, Response $response) { + $response->setContent($response->getContent().'---'); + }); + + $app->match('/', function () { return new Response('foo'); }); + + $request = Request::create('/'); + $this->assertEquals('foo---', $app->handle($request)->getContent()); + } + + public function testAfterFilterCanReturnResponse() + { + $app = new Application(); + + $app->after(function (Request $request, Response $response) { + return new Response('bar'); + }); + + $app->match('/', function () { return new Response('foo'); }); + + $request = Request::create('/'); + $this->assertEquals('bar', $app->handle($request)->getContent()); + } + + public function testRouteAndApplicationMiddlewareParameterInjection() + { + $app = new Application(); + + $test = $this; + + $middlewareTarget = array(); + $applicationBeforeMiddleware = function ($request, $app) use (&$middlewareTarget, $test) { + $test->assertInstanceOf('\Symfony\Component\HttpFoundation\Request', $request); + $test->assertInstanceOf('\Silex\Application', $app); + $middlewareTarget[] = 'application_before_middleware_triggered'; + }; + + $applicationAfterMiddleware = function ($request, $response, $app) use (&$middlewareTarget, $test) { + $test->assertInstanceOf('\Symfony\Component\HttpFoundation\Request', $request); + $test->assertInstanceOf('\Symfony\Component\HttpFoundation\Response', $response); + $test->assertInstanceOf('\Silex\Application', $app); + $middlewareTarget[] = 'application_after_middleware_triggered'; + }; + + $applicationFinishMiddleware = function ($request, $response, $app) use (&$middlewareTarget, $test) { + $test->assertInstanceOf('\Symfony\Component\HttpFoundation\Request', $request); + $test->assertInstanceOf('\Symfony\Component\HttpFoundation\Response', $response); + $test->assertInstanceOf('\Silex\Application', $app); + $middlewareTarget[] = 'application_finish_middleware_triggered'; + }; + + $routeBeforeMiddleware = function ($request, $app) use (&$middlewareTarget, $test) { + $test->assertInstanceOf('\Symfony\Component\HttpFoundation\Request', $request); + $test->assertInstanceOf('\Silex\Application', $app); + $middlewareTarget[] = 'route_before_middleware_triggered'; + }; + + $routeAfterMiddleware = function ($request, $response, $app) use (&$middlewareTarget, $test) { + $test->assertInstanceOf('\Symfony\Component\HttpFoundation\Request', $request); + $test->assertInstanceOf('\Symfony\Component\HttpFoundation\Response', $response); + $test->assertInstanceOf('\Silex\Application', $app); + $middlewareTarget[] = 'route_after_middleware_triggered'; + }; + + $app->before($applicationBeforeMiddleware); + $app->after($applicationAfterMiddleware); + $app->finish($applicationFinishMiddleware); + + $app->match('/', function () { + return new Response('foo'); + }) + ->before($routeBeforeMiddleware) + ->after($routeAfterMiddleware); + + $request = Request::create('/'); + $response = $app->handle($request); + $app->terminate($request, $response); + + $this->assertSame(array('application_before_middleware_triggered', 'route_before_middleware_triggered', 'route_after_middleware_triggered', 'application_after_middleware_triggered', 'application_finish_middleware_triggered'), $middlewareTarget); + } +} diff --git a/tests/Silex/Tests/Provider/AssetServiceProviderTest.php b/tests/Silex/Tests/Provider/AssetServiceProviderTest.php new file mode 100644 index 0000000..7cfc5bb --- /dev/null +++ b/tests/Silex/Tests/Provider/AssetServiceProviderTest.php @@ -0,0 +1,35 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider; + +use Silex\Application; +use Silex\Provider\AssetServiceProvider; + +class AssetServiceProviderTest extends \PHPUnit_Framework_TestCase +{ + public function testGenerateAssetUrl() + { + $app = new Application(); + $app->register(new AssetServiceProvider(), array( + 'assets.version' => 'v1', + 'assets.version_format' => '%s?version=%s', + 'assets.named_packages' => array( + 'css' => array('version' => 'css2', 'base_path' => '/whatever-makes-sense'), + 'images' => array('base_urls' => array('https://img.example.com')), + ), + )); + + $this->assertEquals('/foo.png?version=v1', $app['assets.packages']->getUrl('/foo.png')); + $this->assertEquals('/whatever-makes-sense/foo.css?css2', $app['assets.packages']->getUrl('/foo.css', 'css')); + $this->assertEquals('https://img.example.com/foo.png', $app['assets.packages']->getUrl('/foo.png', 'images')); + } +} diff --git a/tests/Silex/Tests/Provider/DoctrineServiceProviderTest.php b/tests/Silex/Tests/Provider/DoctrineServiceProviderTest.php new file mode 100644 index 0000000..5a7e9a2 --- /dev/null +++ b/tests/Silex/Tests/Provider/DoctrineServiceProviderTest.php @@ -0,0 +1,116 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider; + +use Pimple\Container; +use Silex\Application; +use Silex\Provider\DoctrineServiceProvider; + +/** + * DoctrineProvider test cases. + * + * Fabien Potencier + */ +class DoctrineServiceProviderTest extends \PHPUnit_Framework_TestCase +{ + public function testOptionsInitializer() + { + $app = new Application(); + $app->register(new DoctrineServiceProvider()); + + $this->assertEquals($app['db.default_options'], $app['db']->getParams()); + } + + public function testSingleConnection() + { + if (!in_array('sqlite', \PDO::getAvailableDrivers())) { + $this->markTestSkipped('pdo_sqlite is not available'); + } + + $app = new Application(); + $app->register(new DoctrineServiceProvider(), array( + 'db.options' => array('driver' => 'pdo_sqlite', 'memory' => true), + )); + + $db = $app['db']; + $params = $db->getParams(); + $this->assertTrue(array_key_exists('memory', $params)); + $this->assertTrue($params['memory']); + $this->assertInstanceof('Doctrine\DBAL\Driver\PDOSqlite\Driver', $db->getDriver()); + $this->assertEquals(22, $app['db']->fetchColumn('SELECT 22')); + + $this->assertSame($app['dbs']['default'], $db); + } + + public function testMultipleConnections() + { + if (!in_array('sqlite', \PDO::getAvailableDrivers())) { + $this->markTestSkipped('pdo_sqlite is not available'); + } + + $app = new Application(); + $app->register(new DoctrineServiceProvider(), array( + 'dbs.options' => array( + 'sqlite1' => array('driver' => 'pdo_sqlite', 'memory' => true), + 'sqlite2' => array('driver' => 'pdo_sqlite', 'path' => sys_get_temp_dir().'/silex'), + ), + )); + + $db = $app['db']; + $params = $db->getParams(); + $this->assertTrue(array_key_exists('memory', $params)); + $this->assertTrue($params['memory']); + $this->assertInstanceof('Doctrine\DBAL\Driver\PDOSqlite\Driver', $db->getDriver()); + $this->assertEquals(22, $app['db']->fetchColumn('SELECT 22')); + + $this->assertSame($app['dbs']['sqlite1'], $db); + + $db2 = $app['dbs']['sqlite2']; + $params = $db2->getParams(); + $this->assertTrue(array_key_exists('path', $params)); + $this->assertEquals(sys_get_temp_dir().'/silex', $params['path']); + } + + public function testLoggerLoading() + { + if (!in_array('sqlite', \PDO::getAvailableDrivers())) { + $this->markTestSkipped('pdo_sqlite is not available'); + } + + $app = new Application(); + $this->assertTrue(isset($app['logger'])); + $this->assertNull($app['logger']); + $app->register(new DoctrineServiceProvider(), array( + 'dbs.options' => array( + 'sqlite1' => array('driver' => 'pdo_sqlite', 'memory' => true), + ), + )); + $this->assertEquals(22, $app['db']->fetchColumn('SELECT 22')); + $this->assertNull($app['db']->getConfiguration()->getSQLLogger()); + } + + public function testLoggerNotLoaded() + { + if (!in_array('sqlite', \PDO::getAvailableDrivers())) { + $this->markTestSkipped('pdo_sqlite is not available'); + } + + $app = new Container(); + $app->register(new DoctrineServiceProvider(), array( + 'dbs.options' => array( + 'sqlite1' => array('driver' => 'pdo_sqlite', 'memory' => true), + ), + )); + $this->assertEquals(22, $app['db']->fetchColumn('SELECT 22')); + $this->assertNull($app['db']->getConfiguration()->getSQLLogger()); + } +} diff --git a/tests/Silex/Tests/Provider/FormServiceProviderTest.php b/tests/Silex/Tests/Provider/FormServiceProviderTest.php new file mode 100644 index 0000000..d803bb9 --- /dev/null +++ b/tests/Silex/Tests/Provider/FormServiceProviderTest.php @@ -0,0 +1,355 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider; + +use Silex\Application; +use Silex\Provider\FormServiceProvider; +use Silex\Provider\CsrfServiceProvider; +use Silex\Provider\SessionServiceProvider; +use Silex\Provider\TranslationServiceProvider; +use Silex\Provider\ValidatorServiceProvider; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\FormTypeGuesserChain; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\Translation\Exception\NotFoundResourceException; + +class FormServiceProviderTest extends \PHPUnit_Framework_TestCase +{ + public function testFormFactoryServiceIsFormFactory() + { + $app = new Application(); + $app->register(new FormServiceProvider()); + $this->assertInstanceOf('Symfony\Component\Form\FormFactory', $app['form.factory']); + } + + public function testFormServiceProviderWillLoadTypes() + { + $app = new Application(); + + $app->register(new FormServiceProvider()); + + $app->extend('form.types', function ($extensions) { + $extensions[] = new DummyFormType(); + + return $extensions; + }); + + $form = $app['form.factory']->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', array()) + ->add('dummy', 'Silex\Tests\Provider\DummyFormType') + ->getForm(); + + $this->assertInstanceOf('Symfony\Component\Form\Form', $form); + } + + public function testFormServiceProviderWillLoadTypesServices() + { + $app = new Application(); + + $app->register(new FormServiceProvider()); + + $app['dummy'] = function () { + return new DummyFormType(); + }; + $app->extend('form.types', function ($extensions) { + $extensions[] = 'dummy'; + + return $extensions; + }); + + $form = $app['form.factory'] + ->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', array()) + ->add('dummy', 'dummy') + ->getForm(); + + $this->assertInstanceOf('Symfony\Component\Form\Form', $form); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid form type. The silex service "dummy" does not exist. + */ + public function testNonExistentTypeService() + { + $app = new Application(); + + $app->register(new FormServiceProvider()); + + $app->extend('form.types', function ($extensions) { + $extensions[] = 'dummy'; + + return $extensions; + }); + + $app['form.factory'] + ->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', array()) + ->add('dummy', 'dummy') + ->getForm(); + } + + public function testFormServiceProviderWillLoadTypeExtensions() + { + $app = new Application(); + + $app->register(new FormServiceProvider()); + + $app->extend('form.type.extensions', function ($extensions) { + $extensions[] = new DummyFormTypeExtension(); + + return $extensions; + }); + + $form = $app['form.factory']->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', array()) + ->add('file', 'Symfony\Component\Form\Extension\Core\Type\FileType', array('image_path' => 'webPath')) + ->getForm(); + + $this->assertInstanceOf('Symfony\Component\Form\Form', $form); + } + + public function testFormServiceProviderWillLoadTypeExtensionsServices() + { + $app = new Application(); + + $app->register(new FormServiceProvider()); + + $app['dummy.form.type.extension'] = function () { + return new DummyFormTypeExtension(); + }; + $app->extend('form.type.extensions', function ($extensions) { + $extensions[] = 'dummy.form.type.extension'; + + return $extensions; + }); + + $form = $app['form.factory'] + ->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', array()) + ->add('file', 'Symfony\Component\Form\Extension\Core\Type\FileType', array('image_path' => 'webPath')) + ->getForm(); + + $this->assertInstanceOf('Symfony\Component\Form\Form', $form); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid form type extension. The silex service "dummy.form.type.extension" does not exist. + */ + public function testNonExistentTypeExtensionService() + { + $app = new Application(); + + $app->register(new FormServiceProvider()); + + $app->extend('form.type.extensions', function ($extensions) { + $extensions[] = 'dummy.form.type.extension'; + + return $extensions; + }); + + $app['form.factory'] + ->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', array()) + ->add('dummy', 'dummy.form.type') + ->getForm(); + } + + public function testFormServiceProviderWillLoadTypeGuessers() + { + $app = new Application(); + + $app->register(new FormServiceProvider()); + + $app->extend('form.type.guessers', function ($guessers) { + $guessers[] = new FormTypeGuesserChain(array()); + + return $guessers; + }); + + $this->assertInstanceOf('Symfony\Component\Form\FormFactory', $app['form.factory']); + } + + public function testFormServiceProviderWillLoadTypeGuessersServices() + { + $app = new Application(); + + $app->register(new FormServiceProvider()); + + $app['dummy.form.type.guesser'] = function () { + return new FormTypeGuesserChain(array()); + }; + $app->extend('form.type.guessers', function ($guessers) { + $guessers[] = 'dummy.form.type.guesser'; + + return $guessers; + }); + + $this->assertInstanceOf('Symfony\Component\Form\FormFactory', $app['form.factory']); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid form type guesser. The silex service "dummy.form.type.guesser" does not exist. + */ + public function testNonExistentTypeGuesserService() + { + $app = new Application(); + + $app->register(new FormServiceProvider()); + + $app->extend('form.type.guessers', function ($extensions) { + $extensions[] = 'dummy.form.type.guesser'; + + return $extensions; + }); + + $factory = $app['form.factory']; + } + + public function testFormServiceProviderWillUseTranslatorIfAvailable() + { + $app = new Application(); + + $app->register(new FormServiceProvider()); + $app->register(new TranslationServiceProvider()); + $app['translator.domains'] = array( + 'messages' => array( + 'de' => array( + 'The CSRF token is invalid. Please try to resubmit the form.' => 'German translation', + ), + ), + ); + $app['locale'] = 'de'; + + $app['csrf.token_manager'] = function () { + return $this->getMock('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface'); + }; + + $form = $app['form.factory']->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', array()) + ->getForm(); + + $form->handleRequest($req = Request::create('/', 'POST', array('form' => array( + '_token' => 'the wrong token', + )))); + + $this->assertFalse($form->isValid()); + $r = new \ReflectionMethod($form, 'getErrors'); + if (!$r->getNumberOfParameters()) { + $this->assertContains('ERROR: German translation', $form->getErrorsAsString()); + } else { + // as of 2.5 + $this->assertContains('ERROR: German translation', (string) $form->getErrors(true, false)); + } + } + + public function testFormServiceProviderWillNotAddNonexistentTranslationFiles() + { + $app = new Application(array( + 'locale' => 'nonexistent', + )); + + $app->register(new FormServiceProvider()); + $app->register(new ValidatorServiceProvider()); + $app->register(new TranslationServiceProvider(), array( + 'locale_fallbacks' => array(), + )); + + $app['form.factory']; + $translator = $app['translator']; + + try { + $translator->trans('test'); + } catch (NotFoundResourceException $e) { + $this->fail('Form factory should not add a translation resource that does not exist'); + } + } + + public function testFormCsrf() + { + $app = new Application(); + $app->register(new FormServiceProvider()); + $app->register(new SessionServiceProvider()); + $app->register(new CsrfServiceProvider()); + $app['session.test'] = true; + + $form = $app['form.factory']->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', array())->getForm(); + + $this->assertTrue(isset($form->createView()['_token'])); + } + + public function testUserExtensionCanConfigureDefaultExtensions() + { + $app = new Application(); + $app->register(new FormServiceProvider()); + $app->register(new SessionServiceProvider()); + $app->register(new CsrfServiceProvider()); + $app['session.test'] = true; + + $app->extend('form.type.extensions', function ($extensions) { + $extensions[] = new FormServiceProviderTest\DisableCsrfExtension(); + + return $extensions; + }); + $form = $app['form.factory']->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', array())->getForm(); + + $this->assertFalse($form->getConfig()->getOption('csrf_protection')); + } +} + +if (!class_exists('Symfony\Component\Form\Deprecated\FormEvents')) { + class DummyFormType extends AbstractType + { + } +} else { + // FormTypeInterface::getName() is needed by the form component 2.8.x + class DummyFormType extends AbstractType + { + /** + * @return string The name of this type + */ + public function getName() + { + return 'dummy'; + } + } +} + +if (method_exists('Symfony\Component\Form\AbstractType', 'configureOptions')) { + class DummyFormTypeExtension extends AbstractTypeExtension + { + public function getExtendedType() + { + return 'Symfony\Component\Form\Extension\Core\Type\FileType'; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefined(array('image_path')); + } + } +} else { + class DummyFormTypeExtension extends AbstractTypeExtension + { + public function getExtendedType() + { + return 'Symfony\Component\Form\Extension\Core\Type\FileType'; + } + + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + if (!method_exists($resolver, 'setDefined')) { + $resolver->setOptional(array('image_path')); + } else { + $resolver->setDefined(array('image_path')); + } + } + } +} diff --git a/tests/Silex/Tests/Provider/FormServiceProviderTest/DisableCsrfExtension.php b/tests/Silex/Tests/Provider/FormServiceProviderTest/DisableCsrfExtension.php new file mode 100644 index 0000000..8c82237 --- /dev/null +++ b/tests/Silex/Tests/Provider/FormServiceProviderTest/DisableCsrfExtension.php @@ -0,0 +1,22 @@ +setDefaults(array( + 'csrf_protection' => false, + )); + } + + public function getExtendedType() + { + return FormType::class; + } +} diff --git a/tests/Silex/Tests/Provider/HttpCacheServiceProviderTest.php b/tests/Silex/Tests/Provider/HttpCacheServiceProviderTest.php new file mode 100644 index 0000000..93da4fe --- /dev/null +++ b/tests/Silex/Tests/Provider/HttpCacheServiceProviderTest.php @@ -0,0 +1,80 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider; + +use Silex\Application; +use Silex\Provider\HttpCacheServiceProvider; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * HttpCacheProvider test cases. + * + * @author Igor Wiedler + */ +class HttpCacheServiceProviderTest extends \PHPUnit_Framework_TestCase +{ + public function testRegister() + { + $app = new Application(); + + $app->register(new HttpCacheServiceProvider(), array( + 'http_cache.cache_dir' => sys_get_temp_dir().'/silex_http_cache_'.uniqid(), + )); + + $this->assertInstanceOf('Silex\Provider\HttpCache\HttpCache', $app['http_cache']); + + return $app; + } + + /** + * @depends testRegister + */ + public function testRunCallsShutdown($app) + { + $finished = false; + + $app->finish(function () use (&$finished) { + $finished = true; + }); + + $app->get('/', function () use ($app) { + return new UnsendableResponse('will do something after finish'); + }); + + $request = Request::create('/'); + $app['http_cache']->run($request); + + $this->assertTrue($finished); + } + + public function testDebugDefaultsToThatOfApp() + { + $app = new Application(); + + $app->register(new HttpCacheServiceProvider(), array( + 'http_cache.cache_dir' => sys_get_temp_dir().'/silex_http_cache_'.uniqid(), + )); + + $app['debug'] = true; + $app['http_cache']; + $this->assertTrue($app['http_cache.options']['debug']); + } +} + +class UnsendableResponse extends Response +{ + public function send() + { + // do nothing + } +} diff --git a/tests/Silex/Tests/Provider/HttpFragmentServiceProviderTest.php b/tests/Silex/Tests/Provider/HttpFragmentServiceProviderTest.php new file mode 100644 index 0000000..9a0eb97 --- /dev/null +++ b/tests/Silex/Tests/Provider/HttpFragmentServiceProviderTest.php @@ -0,0 +1,47 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider; + +use Silex\Application; +use Silex\Provider\HttpCacheServiceProvider; +use Silex\Provider\HttpFragmentServiceProvider; +use Silex\Provider\TwigServiceProvider; +use Symfony\Component\HttpFoundation\Request; + +class HttpFragmentServiceProviderTest extends \PHPUnit_Framework_TestCase +{ + public function testRenderFunction() + { + $app = new Application(); + + $app->register(new HttpFragmentServiceProvider()); + $app->register(new HttpCacheServiceProvider(), array('http_cache.cache_dir' => sys_get_temp_dir())); + $app->register(new TwigServiceProvider(), array( + 'twig.templates' => array( + 'hello' => '{{ render("/foo") }}{{ render_esi("/foo") }}{{ render_hinclude("/foo") }}', + 'foo' => 'foo', + ), + )); + + $app->get('/hello', function () use ($app) { + return $app['twig']->render('hello'); + }); + + $app->get('/foo', function () use ($app) { + return $app['twig']->render('foo'); + }); + + $response = $app['http_cache']->handle(Request::create('/hello')); + + $this->assertEquals('foofoo', $response->getContent()); + } +} diff --git a/tests/Silex/Tests/Provider/MonologServiceProviderTest.php b/tests/Silex/Tests/Provider/MonologServiceProviderTest.php new file mode 100644 index 0000000..38d783a --- /dev/null +++ b/tests/Silex/Tests/Provider/MonologServiceProviderTest.php @@ -0,0 +1,218 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider; + +use Monolog\Formatter\JsonFormatter; +use Monolog\Handler\TestHandler; +use Monolog\Logger; +use Silex\Application; +use Silex\Provider\MonologServiceProvider; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Kernel; + +/** + * MonologProvider test cases. + * + * @author Igor Wiedler + */ +class MonologServiceProviderTest extends \PHPUnit_Framework_TestCase +{ + public function testRequestLogging() + { + $app = $this->getApplication(); + + $app->get('/foo', function () use ($app) { + return 'foo'; + }); + + $this->assertFalse($app['monolog.handler']->hasInfoRecords()); + + $request = Request::create('/foo'); + $app->handle($request); + + $this->assertTrue($app['monolog.handler']->hasDebug('> GET /foo')); + $this->assertTrue($app['monolog.handler']->hasDebug('< 200')); + + $records = $app['monolog.handler']->getRecords(); + if (Kernel::VERSION_ID < 30100) { + $this->assertContains('Matched route "GET_foo"', $records[0]['message']); + } else { + $this->assertContains('Matched route "{route}".', $records[0]['message']); + $this->assertSame('GET_foo', $records[0]['context']['route']); + } + } + + public function testManualLogging() + { + $app = $this->getApplication(); + + $app->get('/log', function () use ($app) { + $app['monolog']->addDebug('logging a message'); + }); + + $this->assertFalse($app['monolog.handler']->hasDebugRecords()); + + $request = Request::create('/log'); + $app->handle($request); + + $this->assertTrue($app['monolog.handler']->hasDebug('logging a message')); + } + + public function testOverrideFormatter() + { + $app = new Application(); + + $app->register(new MonologServiceProvider(), array( + 'monolog.formatter' => new JsonFormatter(), + 'monolog.logfile' => 'php://memory', + )); + + $this->assertInstanceOf('Monolog\Formatter\JsonFormatter', $app['monolog.handler']->getFormatter()); + } + + public function testErrorLogging() + { + $app = $this->getApplication(); + + $app->error(function (\Exception $e) { + return 'error handled'; + }); + + /* + * Simulate 404, logged to error level + */ + $this->assertFalse($app['monolog.handler']->hasErrorRecords()); + + $request = Request::create('/error'); + $app->handle($request); + + $records = $app['monolog.handler']->getRecords(); + $pattern = "#Symfony\\\\Component\\\\HttpKernel\\\\Exception\\\\NotFoundHttpException: No route found for \"GET /error\" \(uncaught exception\) at .* line \d+#"; + $this->assertMatchingRecord($pattern, Logger::ERROR, $app['monolog.handler']); + + /* + * Simulate unhandled exception, logged to critical + */ + $app->get('/error', function () { + throw new \RuntimeException('very bad error'); + }); + + $this->assertFalse($app['monolog.handler']->hasCriticalRecords()); + + $request = Request::create('/error'); + $app->handle($request); + + $pattern = "#RuntimeException: very bad error \(uncaught exception\) at .* line \d+#"; + $this->assertMatchingRecord($pattern, Logger::CRITICAL, $app['monolog.handler']); + } + + public function testRedirectLogging() + { + $app = $this->getApplication(); + + $app->get('/foo', function () use ($app) { + return new RedirectResponse('/bar', 302); + }); + + $this->assertFalse($app['monolog.handler']->hasInfoRecords()); + + $request = Request::create('/foo'); + $app->handle($request); + + $this->assertTrue($app['monolog.handler']->hasDebug('< 302 /bar')); + } + + public function testErrorLoggingGivesWayToSecurityExceptionHandling() + { + $app = $this->getApplication(); + $app['monolog.level'] = Logger::ERROR; + + $app->register(new \Silex\Provider\SecurityServiceProvider(), array( + 'security.firewalls' => array( + 'admin' => array( + 'pattern' => '^/admin', + 'http' => true, + 'users' => array(), + ), + ), + )); + + $app->get('/admin', function () { + return 'SECURE!'; + }); + + $request = Request::create('/admin'); + $app->run($request); + + $this->assertEmpty($app['monolog.handler']->getRecords(), 'Expected no logging to occur'); + } + + public function testStringErrorLevel() + { + $app = $this->getApplication(); + $app['monolog.level'] = 'info'; + + $this->assertSame(Logger::INFO, $app['monolog.handler']->getLevel()); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Provided logging level 'foo' does not exist. Must be a valid monolog logging level. + */ + public function testNonExistentStringErrorLevel() + { + $app = $this->getApplication(); + $app['monolog.level'] = 'foo'; + + $app['monolog.handler']->getLevel(); + } + + public function testDisableListener() + { + $app = $this->getApplication(); + unset($app['monolog.listener']); + + $app->handle(Request::create('/404')); + + $this->assertEmpty($app['monolog.handler']->getRecords(), 'Expected no logging to occur'); + } + + protected function assertMatchingRecord($pattern, $level, $handler) + { + $found = false; + $records = $handler->getRecords(); + foreach ($records as $record) { + if (preg_match($pattern, $record['message']) && $record['level'] == $level) { + $found = true; + continue; + } + } + $this->assertTrue($found, "Trying to find record matching $pattern with level $level"); + } + + protected function getApplication() + { + $app = new Application(); + + $app->register(new MonologServiceProvider(), array( + 'monolog.handler' => function () use ($app) { + $level = MonologServiceProvider::translateLevel($app['monolog.level']); + + return new TestHandler($level); + }, + 'monolog.logfile' => 'php://memory', + )); + + return $app; + } +} diff --git a/tests/Silex/Tests/Provider/RememberMeServiceProviderTest.php b/tests/Silex/Tests/Provider/RememberMeServiceProviderTest.php new file mode 100644 index 0000000..b027497 --- /dev/null +++ b/tests/Silex/Tests/Provider/RememberMeServiceProviderTest.php @@ -0,0 +1,107 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider; + +use Silex\Application; +use Silex\WebTestCase; +use Silex\Provider\RememberMeServiceProvider; +use Silex\Provider\SecurityServiceProvider; +use Silex\Provider\SessionServiceProvider; +use Symfony\Component\HttpKernel\Client; +use Symfony\Component\Security\Http\SecurityEvents; + +/** + * SecurityServiceProvider. + * + * @author Fabien Potencier + */ +class RememberMeServiceProviderTest extends WebTestCase +{ + public function testRememberMeAuthentication() + { + $app = $this->createApplication(); + + $interactiveLogin = new InteractiveLoginTriggered(); + $app->on(SecurityEvents::INTERACTIVE_LOGIN, array($interactiveLogin, 'onInteractiveLogin')); + + $client = new Client($app); + + $client->request('get', '/'); + $this->assertFalse($interactiveLogin->triggered, 'The interactive login has not been triggered yet'); + $client->request('post', '/login_check', array('_username' => 'fabien', '_password' => 'foo', '_remember_me' => 'true')); + $client->followRedirect(); + $this->assertEquals('AUTHENTICATED_FULLY', $client->getResponse()->getContent()); + $this->assertTrue($interactiveLogin->triggered, 'The interactive login has been triggered'); + + $this->assertNotNull($client->getCookiejar()->get('REMEMBERME'), 'The REMEMBERME cookie is set'); + $event = false; + + $client->getCookiejar()->expire('MOCKSESSID'); + + $client->request('get', '/'); + $this->assertEquals('AUTHENTICATED_REMEMBERED', $client->getResponse()->getContent()); + $this->assertTrue($interactiveLogin->triggered, 'The interactive login has been triggered'); + + $client->request('get', '/logout'); + $client->followRedirect(); + + $this->assertNull($client->getCookiejar()->get('REMEMBERME'), 'The REMEMBERME cookie has been removed'); + } + + public function createApplication($authenticationMethod = 'form') + { + $app = new Application(); + + $app['debug'] = true; + unset($app['exception_handler']); + + $app->register(new SessionServiceProvider(), array( + 'session.test' => true, + )); + $app->register(new SecurityServiceProvider()); + $app->register(new RememberMeServiceProvider()); + + $app['security.firewalls'] = array( + 'http-auth' => array( + 'pattern' => '^.*$', + 'form' => true, + 'remember_me' => array(), + 'logout' => true, + 'users' => array( + 'fabien' => array('ROLE_USER', '$2y$15$lzUNsTegNXvZW3qtfucV0erYBcEqWVeyOmjolB7R1uodsAVJ95vvu'), + ), + ), + ); + + $app->get('/', function () use ($app) { + if ($app['security.authorization_checker']->isGranted('IS_AUTHENTICATED_FULLY')) { + return 'AUTHENTICATED_FULLY'; + } elseif ($app['security.authorization_checker']->isGranted('IS_AUTHENTICATED_REMEMBERED')) { + return 'AUTHENTICATED_REMEMBERED'; + } else { + return 'AUTHENTICATED_ANONYMOUSLY'; + } + }); + + return $app; + } +} + +class InteractiveLoginTriggered +{ + public $triggered = false; + + public function onInteractiveLogin() + { + $this->triggered = true; + } +} diff --git a/tests/Silex/Tests/Provider/RoutingServiceProviderTest.php b/tests/Silex/Tests/Provider/RoutingServiceProviderTest.php new file mode 100644 index 0000000..da2ca78 --- /dev/null +++ b/tests/Silex/Tests/Provider/RoutingServiceProviderTest.php @@ -0,0 +1,121 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider; + +use Pimple\Container; +use Silex\Application; +use Silex\Provider\RoutingServiceProvider; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + +/** + * RoutingProvider test cases. + * + * @author Igor Wiedler + */ +class RoutingServiceProviderTest extends \PHPUnit_Framework_TestCase +{ + public function testRegister() + { + $app = new Application(); + + $app->get('/hello/{name}', function ($name) {}) + ->bind('hello'); + + $app->get('/', function () {}); + + $request = Request::create('/'); + $app->handle($request); + + $this->assertInstanceOf('Symfony\Component\Routing\Generator\UrlGenerator', $app['url_generator']); + } + + public function testUrlGeneration() + { + $app = new Application(); + + $app->get('/hello/{name}', function ($name) {}) + ->bind('hello'); + + $app->get('/', function () use ($app) { + return $app['url_generator']->generate('hello', array('name' => 'john')); + }); + + $request = Request::create('/'); + $response = $app->handle($request); + + $this->assertEquals('/hello/john', $response->getContent()); + } + + public function testAbsoluteUrlGeneration() + { + $app = new Application(); + + $app->get('/hello/{name}', function ($name) {}) + ->bind('hello'); + + $app->get('/', function () use ($app) { + return $app['url_generator']->generate('hello', array('name' => 'john'), UrlGeneratorInterface::ABSOLUTE_URL); + }); + + $request = Request::create('https://localhost:81/'); + $response = $app->handle($request); + + $this->assertEquals('https://localhost:81/hello/john', $response->getContent()); + } + + public function testUrlGenerationWithHttp() + { + $app = new Application(); + + $app->get('/insecure', function () {}) + ->bind('insecure_page') + ->requireHttp(); + + $app->get('/', function () use ($app) { + return $app['url_generator']->generate('insecure_page'); + }); + + $request = Request::create('https://localhost/'); + $response = $app->handle($request); + + $this->assertEquals('http://localhost/insecure', $response->getContent()); + } + + public function testUrlGenerationWithHttps() + { + $app = new Application(); + + $app->get('/secure', function () {}) + ->bind('secure_page') + ->requireHttps(); + + $app->get('/', function () use ($app) { + return $app['url_generator']->generate('secure_page'); + }); + + $request = Request::create('http://localhost/'); + $response = $app->handle($request); + + $this->assertEquals('https://localhost/secure', $response->getContent()); + } + + public function testControllersFactory() + { + $app = new Container(); + $app->register(new RoutingServiceProvider()); + $coll = $app['controllers_factory']; + $coll->mount('/blog', function ($blog) { + $this->assertInstanceOf('Silex\ControllerCollection', $blog); + }); + } +} diff --git a/tests/Silex/Tests/Provider/SecurityServiceProviderTest.php b/tests/Silex/Tests/Provider/SecurityServiceProviderTest.php new file mode 100644 index 0000000..b9ea3cc --- /dev/null +++ b/tests/Silex/Tests/Provider/SecurityServiceProviderTest.php @@ -0,0 +1,440 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider; + +use Silex\Application; +use Silex\WebTestCase; +use Silex\Provider\SecurityServiceProvider; +use Silex\Provider\SessionServiceProvider; +use Silex\Provider\ValidatorServiceProvider; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\HttpKernel\Client; +use Symfony\Component\HttpFoundation\Request; + +/** + * SecurityServiceProvider. + * + * @author Fabien Potencier + */ +class SecurityServiceProviderTest extends WebTestCase +{ + /** + * @expectedException \LogicException + */ + public function testWrongAuthenticationType() + { + $app = new Application(); + $app->register(new SecurityServiceProvider(), array( + 'security.firewalls' => array( + 'wrong' => array( + 'foobar' => true, + 'users' => array(), + ), + ), + )); + $app->get('/', function () {}); + $app->handle(Request::create('/')); + } + + public function testFormAuthentication() + { + $app = $this->createApplication('form'); + + $client = new Client($app); + + $client->request('get', '/'); + $this->assertEquals('ANONYMOUS', $client->getResponse()->getContent()); + + $client->request('post', '/login_check', array('_username' => 'fabien', '_password' => 'bar')); + $this->assertContains('Bad credentials', $app['security.last_error']($client->getRequest())); + // hack to re-close the session as the previous assertions re-opens it + $client->getRequest()->getSession()->save(); + + $client->request('post', '/login_check', array('_username' => 'fabien', '_password' => 'foo')); + $this->assertEquals('', $app['security.last_error']($client->getRequest())); + $client->getRequest()->getSession()->save(); + $this->assertEquals(302, $client->getResponse()->getStatusCode()); + $this->assertEquals('http://localhost/', $client->getResponse()->getTargetUrl()); + + $client->request('get', '/'); + $this->assertEquals('fabienAUTHENTICATED', $client->getResponse()->getContent()); + $client->request('get', '/admin'); + $this->assertEquals(403, $client->getResponse()->getStatusCode()); + + $client->request('get', '/logout'); + $this->assertEquals(302, $client->getResponse()->getStatusCode()); + $this->assertEquals('http://localhost/', $client->getResponse()->getTargetUrl()); + + $client->request('get', '/'); + $this->assertEquals('ANONYMOUS', $client->getResponse()->getContent()); + + $client->request('get', '/admin'); + $this->assertEquals(302, $client->getResponse()->getStatusCode()); + $this->assertEquals('http://localhost/login', $client->getResponse()->getTargetUrl()); + + $client->request('post', '/login_check', array('_username' => 'admin', '_password' => 'foo')); + $this->assertEquals('', $app['security.last_error']($client->getRequest())); + $client->getRequest()->getSession()->save(); + $this->assertEquals(302, $client->getResponse()->getStatusCode()); + $this->assertEquals('http://localhost/admin', $client->getResponse()->getTargetUrl()); + + $client->request('get', '/'); + $this->assertEquals('adminAUTHENTICATEDADMIN', $client->getResponse()->getContent()); + $client->request('get', '/admin'); + $this->assertEquals('admin', $client->getResponse()->getContent()); + } + + public function testHttpAuthentication() + { + $app = $this->createApplication('http'); + + $client = new Client($app); + + $client->request('get', '/'); + $this->assertEquals(401, $client->getResponse()->getStatusCode()); + $this->assertEquals('Basic realm="Secured"', $client->getResponse()->headers->get('www-authenticate')); + + $client->request('get', '/', array(), array(), array('PHP_AUTH_USER' => 'dennis', 'PHP_AUTH_PW' => 'foo')); + $this->assertEquals('dennisAUTHENTICATED', $client->getResponse()->getContent()); + $client->request('get', '/admin'); + $this->assertEquals(403, $client->getResponse()->getStatusCode()); + + $client->restart(); + + $client->request('get', '/'); + $this->assertEquals(401, $client->getResponse()->getStatusCode()); + $this->assertEquals('Basic realm="Secured"', $client->getResponse()->headers->get('www-authenticate')); + + $client->request('get', '/', array(), array(), array('PHP_AUTH_USER' => 'admin', 'PHP_AUTH_PW' => 'foo')); + $this->assertEquals('adminAUTHENTICATEDADMIN', $client->getResponse()->getContent()); + $client->request('get', '/admin'); + $this->assertEquals('admin', $client->getResponse()->getContent()); + } + + public function testGuardAuthentication() + { + $app = $this->createApplication('guard'); + + $client = new Client($app); + + $client->request('get', '/'); + $this->assertEquals(401, $client->getResponse()->getStatusCode(), 'The entry point is configured'); + $this->assertEquals('{"message":"Authentication Required"}', $client->getResponse()->getContent()); + + $client->request('get', '/', array(), array(), array('HTTP_X_AUTH_TOKEN' => 'lili:not the secret')); + $this->assertEquals(403, $client->getResponse()->getStatusCode(), 'User not found'); + $this->assertEquals('{"message":"Username could not be found."}', $client->getResponse()->getContent()); + + $client->request('get', '/', array(), array(), array('HTTP_X_AUTH_TOKEN' => 'victoria:not the secret')); + $this->assertEquals(403, $client->getResponse()->getStatusCode(), 'Invalid credentials'); + $this->assertEquals('{"message":"Invalid credentials."}', $client->getResponse()->getContent()); + + $client->request('get', '/', array(), array(), array('HTTP_X_AUTH_TOKEN' => 'victoria:victoriasecret')); + $this->assertEquals('victoria', $client->getResponse()->getContent()); + } + + public function testUserPasswordValidatorIsRegistered() + { + $app = new Application(); + + $app->register(new ValidatorServiceProvider()); + $app->register(new SecurityServiceProvider(), array( + 'security.firewalls' => array( + 'admin' => array( + 'pattern' => '^/admin', + 'http' => true, + 'users' => array( + 'admin' => array('ROLE_ADMIN', '513aeb0121909'), + ), + ), + ), + )); + + $app->boot(); + + $this->assertInstanceOf('Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator', $app['security.validator.user_password_validator']); + } + + public function testExposedExceptions() + { + $app = $this->createApplication('form'); + $app['security.hide_user_not_found'] = false; + + $client = new Client($app); + + $client->request('get', '/'); + $this->assertEquals('ANONYMOUS', $client->getResponse()->getContent()); + + $client->request('post', '/login_check', array('_username' => 'fabien', '_password' => 'bar')); + $this->assertEquals('The presented password is invalid.', $app['security.last_error']($client->getRequest())); + $client->getRequest()->getSession()->save(); + + $client->request('post', '/login_check', array('_username' => 'unknown', '_password' => 'bar')); + $this->assertEquals('Username "unknown" does not exist.', $app['security.last_error']($client->getRequest())); + $client->getRequest()->getSession()->save(); + } + + public function testFakeRoutesAreSerializable() + { + $app = new Application(); + + $app->register(new SecurityServiceProvider(), array( + 'security.firewalls' => array( + 'admin' => array( + 'logout' => true, + ), + ), + )); + + $app->boot(); + $app->flush(); + + $this->assertCount(1, unserialize(serialize($app['routes']))); + } + + public function testUser() + { + $app = new Application(); + $app->register(new SecurityServiceProvider(), array( + 'security.firewalls' => array( + 'default' => array( + 'http' => true, + 'users' => array( + 'fabien' => array('ROLE_ADMIN', '$2y$15$lzUNsTegNXvZW3qtfucV0erYBcEqWVeyOmjolB7R1uodsAVJ95vvu'), + ), + ), + ), + )); + $app->get('/', function () { return 'foo'; }); + + $request = Request::create('/'); + $app->handle($request); + $this->assertNull($app['user']); + + $request->headers->set('PHP_AUTH_USER', 'fabien'); + $request->headers->set('PHP_AUTH_PW', 'foo'); + $app->handle($request); + $this->assertInstanceOf('Symfony\Component\Security\Core\User\UserInterface', $app['user']); + $this->assertEquals('fabien', $app['user']->getUsername()); + } + + public function testUserWithNoToken() + { + $app = new Application(); + $app->register(new SecurityServiceProvider(), array( + 'security.firewalls' => array( + 'default' => array( + 'http' => true, + ), + ), + )); + + $request = Request::create('/'); + + $app->get('/', function () { return 'foo'; }); + $app->handle($request); + $this->assertNull($app['user']); + } + + public function testUserWithInvalidUser() + { + $app = new Application(); + $app->register(new SecurityServiceProvider(), array( + 'security.firewalls' => array( + 'default' => array( + 'http' => true, + ), + ), + )); + + $request = Request::create('/'); + $app->boot(); + $app['security.token_storage']->setToken(new UsernamePasswordToken('foo', 'foo', 'foo')); + + $app->get('/', function () { return 'foo'; }); + $app->handle($request); + $this->assertNull($app['user']); + } + + public function testAccessRulePathArray() + { + $app = new Application(); + $app->register(new SecurityServiceProvider(), array( + 'security.firewalls' => array( + 'default' => array( + 'http' => true, + ), + ), + 'security.access_rules' => array( + array(array( + 'path' => '^/admin', + ), 'ROLE_ADMIN'), + ), + )); + + $request = Request::create('/admin'); + $app->boot(); + $accessMap = $app['security.access_map']; + $this->assertEquals($accessMap->getPatterns($request), array( + array('ROLE_ADMIN'), + '', + )); + } + + public function createApplication($authenticationMethod = 'form') + { + $app = new Application(); + $app->register(new SessionServiceProvider()); + + $app = call_user_func(array($this, 'add'.ucfirst($authenticationMethod).'Authentication'), $app); + + $app['session.test'] = true; + + return $app; + } + + private function addFormAuthentication($app) + { + $app->register(new SecurityServiceProvider(), array( + 'security.firewalls' => array( + 'login' => array( + 'pattern' => '^/login$', + ), + 'default' => array( + 'pattern' => '^.*$', + 'anonymous' => true, + 'form' => array( + 'require_previous_session' => false, + ), + 'logout' => true, + 'users' => array( + // password is foo + 'fabien' => array('ROLE_USER', '$2y$15$lzUNsTegNXvZW3qtfucV0erYBcEqWVeyOmjolB7R1uodsAVJ95vvu'), + 'admin' => array('ROLE_ADMIN', '$2y$15$lzUNsTegNXvZW3qtfucV0erYBcEqWVeyOmjolB7R1uodsAVJ95vvu'), + ), + ), + ), + 'security.access_rules' => array( + array('^/admin', 'ROLE_ADMIN'), + ), + 'security.role_hierarchy' => array( + 'ROLE_ADMIN' => array('ROLE_USER'), + ), + )); + + $app->get('/login', function (Request $request) use ($app) { + $app['session']->start(); + + return $app['security.last_error']($request); + }); + + $app->get('/', function () use ($app) { + $user = $app['security.token_storage']->getToken()->getUser(); + + $content = is_object($user) ? $user->getUsername() : 'ANONYMOUS'; + + if ($app['security.authorization_checker']->isGranted('IS_AUTHENTICATED_FULLY')) { + $content .= 'AUTHENTICATED'; + } + + if ($app['security.authorization_checker']->isGranted('ROLE_ADMIN')) { + $content .= 'ADMIN'; + } + + return $content; + }); + + $app->get('/admin', function () use ($app) { + return 'admin'; + }); + + return $app; + } + + private function addHttpAuthentication($app) + { + $app->register(new SecurityServiceProvider(), array( + 'security.firewalls' => array( + 'http-auth' => array( + 'pattern' => '^.*$', + 'http' => true, + 'users' => array( + // password is foo + 'dennis' => array('ROLE_USER', '$2y$15$lzUNsTegNXvZW3qtfucV0erYBcEqWVeyOmjolB7R1uodsAVJ95vvu'), + 'admin' => array('ROLE_ADMIN', '$2y$15$lzUNsTegNXvZW3qtfucV0erYBcEqWVeyOmjolB7R1uodsAVJ95vvu'), + ), + ), + ), + 'security.access_rules' => array( + array('^/admin', 'ROLE_ADMIN'), + ), + 'security.role_hierarchy' => array( + 'ROLE_ADMIN' => array('ROLE_USER'), + ), + )); + + $app->get('/', function () use ($app) { + $user = $app['security.token_storage']->getToken()->getUser(); + $content = is_object($user) ? $user->getUsername() : 'ANONYMOUS'; + + if ($app['security.authorization_checker']->isGranted('IS_AUTHENTICATED_FULLY')) { + $content .= 'AUTHENTICATED'; + } + + if ($app['security.authorization_checker']->isGranted('ROLE_ADMIN')) { + $content .= 'ADMIN'; + } + + return $content; + }); + + $app->get('/admin', function () use ($app) { + return 'admin'; + }); + + return $app; + } + + private function addGuardAuthentication($app) + { + $app['app.authenticator.token'] = function ($app) { + return new SecurityServiceProviderTest\TokenAuthenticator($app); + }; + + $app->register(new SecurityServiceProvider(), array( + 'security.firewalls' => array( + 'guard' => array( + 'pattern' => '^.*$', + 'form' => true, + 'guard' => array( + 'authenticators' => array( + 'app.authenticator.token', + ), + ), + 'users' => array( + 'victoria' => array('ROLE_USER', 'victoriasecret'), + ), + ), + ), + )); + + $app->get('/', function () use ($app) { + $user = $app['security.token_storage']->getToken()->getUser(); + + $content = is_object($user) ? $user->getUsername() : 'ANONYMOUS'; + + return $content; + })->bind('homepage'); + + return $app; + } +} diff --git a/tests/Silex/Tests/Provider/SecurityServiceProviderTest/TokenAuthenticator.php b/tests/Silex/Tests/Provider/SecurityServiceProviderTest/TokenAuthenticator.php new file mode 100644 index 0000000..c569428 --- /dev/null +++ b/tests/Silex/Tests/Provider/SecurityServiceProviderTest/TokenAuthenticator.php @@ -0,0 +1,79 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider\SecurityServiceProviderTest; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; + +/** + * This class is used to test "guard" authentication with the SecurityServiceProvider. + */ +class TokenAuthenticator extends AbstractGuardAuthenticator +{ + public function getCredentials(Request $request) + { + if (!$token = $request->headers->get('X-AUTH-TOKEN')) { + return; + } + + list($username, $secret) = explode(':', $token); + + return array( + 'username' => $username, + 'secret' => $secret, + ); + } + + public function getUser($credentials, UserProviderInterface $userProvider) + { + return $userProvider->loadUserByUsername($credentials['username']); + } + + public function checkCredentials($credentials, UserInterface $user) + { + // This is not a safe way of validating a password. + return $user->getPassword() === $credentials['secret']; + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + { + return; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + { + $data = array( + 'message' => strtr($exception->getMessageKey(), $exception->getMessageData()), + ); + + return new JsonResponse($data, 403); + } + + public function start(Request $request, AuthenticationException $authException = null) + { + $data = array( + 'message' => 'Authentication Required', + ); + + return new JsonResponse($data, 401); + } + + public function supportsRememberMe() + { + return false; + } +} diff --git a/tests/Silex/Tests/Provider/SerializerServiceProviderTest.php b/tests/Silex/Tests/Provider/SerializerServiceProviderTest.php new file mode 100644 index 0000000..0b143f5 --- /dev/null +++ b/tests/Silex/Tests/Provider/SerializerServiceProviderTest.php @@ -0,0 +1,36 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider; + +use Silex\Application; +use Silex\Provider\SerializerServiceProvider; + +/** + * SerializerServiceProvider test cases. + * + * @author Fabien Potencier + */ +class SerializerServiceProviderTest extends \PHPUnit_Framework_TestCase +{ + public function testRegister() + { + $app = new Application(); + + $app->register(new SerializerServiceProvider()); + + $this->assertInstanceOf("Symfony\Component\Serializer\Serializer", $app['serializer']); + $this->assertTrue($app['serializer']->supportsEncoding('xml')); + $this->assertTrue($app['serializer']->supportsEncoding('json')); + $this->assertTrue($app['serializer']->supportsDecoding('xml')); + $this->assertTrue($app['serializer']->supportsDecoding('json')); + } +} diff --git a/tests/Silex/Tests/Provider/SessionServiceProviderTest.php b/tests/Silex/Tests/Provider/SessionServiceProviderTest.php new file mode 100644 index 0000000..fb7ae0c --- /dev/null +++ b/tests/Silex/Tests/Provider/SessionServiceProviderTest.php @@ -0,0 +1,126 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider; + +use Silex\Application; +use Silex\WebTestCase; +use Silex\Provider\SessionServiceProvider; +use Symfony\Component\HttpKernel\Client; +use Symfony\Component\HttpFoundation\Session; + +/** + * SessionProvider test cases. + * + * @author Igor Wiedler + * @author Fabien Potencier + */ +class SessionServiceProviderTest extends WebTestCase +{ + public function testRegister() + { + /* + * Smoke test + */ + $defaultStorage = $this->app['session.storage.native']; + + $client = $this->createClient(); + + $client->request('get', '/login'); + $this->assertEquals('Logged in successfully.', $client->getResponse()->getContent()); + + $client->request('get', '/account'); + $this->assertEquals('This is your account.', $client->getResponse()->getContent()); + + $client->request('get', '/logout'); + $this->assertEquals('Logged out successfully.', $client->getResponse()->getContent()); + + $client->request('get', '/account'); + $this->assertEquals('You are not logged in.', $client->getResponse()->getContent()); + } + + public function createApplication() + { + $app = new Application(); + + $app->register(new SessionServiceProvider(), array( + 'session.test' => true, + )); + + $app->get('/login', function () use ($app) { + $app['session']->set('logged_in', true); + + return 'Logged in successfully.'; + }); + + $app->get('/account', function () use ($app) { + if (!$app['session']->get('logged_in')) { + return 'You are not logged in.'; + } + + return 'This is your account.'; + }); + + $app->get('/logout', function () use ($app) { + $app['session']->invalidate(); + + return 'Logged out successfully.'; + }); + + return $app; + } + + public function testWithRoutesThatDoesNotUseSession() + { + $app = new Application(); + + $app->register(new SessionServiceProvider(), array( + 'session.test' => true, + )); + + $app->get('/', function () { + return 'A welcome page.'; + }); + + $app->get('/robots.txt', function () { + return 'Informations for robots.'; + }); + + $app['debug'] = true; + unset($app['exception_handler']); + + $client = new Client($app); + + $client->request('get', '/'); + $this->assertEquals('A welcome page.', $client->getResponse()->getContent()); + + $client->request('get', '/robots.txt'); + $this->assertEquals('Informations for robots.', $client->getResponse()->getContent()); + } + + public function testSessionRegister() + { + $app = new Application(); + + $attrs = new Session\Attribute\AttributeBag(); + $flash = new Session\Flash\FlashBag(); + $app->register(new SessionServiceProvider(), array( + 'session.attribute_bag' => $attrs, + 'session.flash_bag' => $flash, + 'session.test' => true, + )); + + $session = $app['session']; + + $this->assertSame($flash, $session->getBag('flashes')); + $this->assertSame($attrs, $session->getBag('attributes')); + } +} diff --git a/tests/Silex/Tests/Provider/SpoolStub.php b/tests/Silex/Tests/Provider/SpoolStub.php new file mode 100644 index 0000000..006fc06 --- /dev/null +++ b/tests/Silex/Tests/Provider/SpoolStub.php @@ -0,0 +1,47 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider; + +class SpoolStub implements \Swift_Spool +{ + private $messages = array(); + public $hasFlushed = false; + + public function getMessages() + { + return $this->messages; + } + + public function start() + { + } + + public function stop() + { + } + + public function isStarted() + { + return count($this->messages) > 0; + } + + public function queueMessage(\Swift_Mime_Message $message) + { + $this->messages[] = $message; + } + + public function flushQueue(\Swift_Transport $transport, &$failedRecipients = null) + { + $this->hasFlushed = true; + $this->messages = array(); + } +} diff --git a/tests/Silex/Tests/Provider/SwiftmailerServiceProviderTest.php b/tests/Silex/Tests/Provider/SwiftmailerServiceProviderTest.php new file mode 100644 index 0000000..bdf4d8a --- /dev/null +++ b/tests/Silex/Tests/Provider/SwiftmailerServiceProviderTest.php @@ -0,0 +1,92 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider; + +use Silex\Application; +use Silex\Provider\SwiftmailerServiceProvider; +use Symfony\Component\HttpFoundation\Request; + +class SwiftmailerServiceProviderTest extends \PHPUnit_Framework_TestCase +{ + public function testSwiftMailerServiceIsSwiftMailer() + { + $app = new Application(); + + $app->register(new SwiftmailerServiceProvider()); + $app->boot(); + + $this->assertInstanceOf('Swift_Mailer', $app['mailer']); + } + + public function testSwiftMailerIgnoresSpoolIfDisabled() + { + $app = new Application(); + + $app->register(new SwiftmailerServiceProvider()); + $app->boot(); + + $app['swiftmailer.use_spool'] = false; + + $app['swiftmailer.spooltransport'] = function () { + throw new \Exception('Should not be instantiated'); + }; + + $this->assertInstanceOf('Swift_Mailer', $app['mailer']); + } + + public function testSwiftMailerSendsMailsOnFinish() + { + $app = new Application(); + + $app->register(new SwiftmailerServiceProvider()); + $app->boot(); + + $app['swiftmailer.spool'] = function () { + return new SpoolStub(); + }; + + $app->get('/', function () use ($app) { + $app['mailer']->send(\Swift_Message::newInstance()); + }); + + $this->assertCount(0, $app['swiftmailer.spool']->getMessages()); + + $request = Request::create('/'); + $response = $app->handle($request); + $this->assertCount(1, $app['swiftmailer.spool']->getMessages()); + + $app->terminate($request, $response); + $this->assertTrue($app['swiftmailer.spool']->hasFlushed); + $this->assertCount(0, $app['swiftmailer.spool']->getMessages()); + } + + public function testSwiftMailerAvoidsFlushesIfMailerIsUnused() + { + $app = new Application(); + + $app->register(new SwiftmailerServiceProvider()); + $app->boot(); + + $app['swiftmailer.spool'] = function () { + return new SpoolStub(); + }; + + $app->get('/', function () use ($app) { }); + + $request = Request::create('/'); + $response = $app->handle($request); + $this->assertCount(0, $app['swiftmailer.spool']->getMessages()); + + $app->terminate($request, $response); + $this->assertFalse($app['swiftmailer.spool']->hasFlushed); + } +} diff --git a/tests/Silex/Tests/Provider/TranslationServiceProviderTest.php b/tests/Silex/Tests/Provider/TranslationServiceProviderTest.php new file mode 100644 index 0000000..6d893f9 --- /dev/null +++ b/tests/Silex/Tests/Provider/TranslationServiceProviderTest.php @@ -0,0 +1,181 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider; + +use Silex\Application; +use Silex\Provider\TranslationServiceProvider; +use Silex\Provider\LocaleServiceProvider; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +/** + * TranslationProvider test cases. + * + * @author Daniel Tschinder + */ +class TranslationServiceProviderTest extends \PHPUnit_Framework_TestCase +{ + /** + * @return Application + */ + protected function getPreparedApp() + { + $app = new Application(); + + $app->register(new LocaleServiceProvider()); + $app->register(new TranslationServiceProvider()); + $app['translator.domains'] = array( + 'messages' => array( + 'en' => array( + 'key1' => 'The translation', + 'key_only_english' => 'Foo', + 'key2' => 'One apple|%count% apples', + 'test' => array( + 'key' => 'It works', + ), + ), + 'de' => array( + 'key1' => 'The german translation', + 'key2' => 'One german apple|%count% german apples', + 'test' => array( + 'key' => 'It works in german', + ), + ), + ), + ); + + return $app; + } + + public function transChoiceProvider() + { + return array( + array('key2', 0, null, '0 apples'), + array('key2', 1, null, 'One apple'), + array('key2', 2, null, '2 apples'), + array('key2', 0, 'de', '0 german apples'), + array('key2', 1, 'de', 'One german apple'), + array('key2', 2, 'de', '2 german apples'), + array('key2', 0, 'ru', '0 apples'), // fallback + array('key2', 1, 'ru', 'One apple'), // fallback + array('key2', 2, 'ru', '2 apples'), // fallback + ); + } + + public function transProvider() + { + return array( + array('key1', null, 'The translation'), + array('key1', 'de', 'The german translation'), + array('key1', 'ru', 'The translation'), // fallback + array('test.key', null, 'It works'), + array('test.key', 'de', 'It works in german'), + array('test.key', 'ru', 'It works'), // fallback + ); + } + + /** + * @dataProvider transProvider + */ + public function testTransForDefaultLanguage($key, $locale, $expected) + { + $app = $this->getPreparedApp(); + + $result = $app['translator']->trans($key, array(), null, $locale); + + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider transChoiceProvider + */ + public function testTransChoiceForDefaultLanguage($key, $number, $locale, $expected) + { + $app = $this->getPreparedApp(); + + $result = $app['translator']->transChoice($key, $number, array('%count%' => $number), null, $locale); + $this->assertEquals($expected, $result); + } + + public function testFallbacks() + { + $app = $this->getPreparedApp(); + $app['locale_fallbacks'] = array('de', 'en'); + + // fallback to english + $result = $app['translator']->trans('key_only_english', array(), null, 'ru'); + $this->assertEquals('Foo', $result); + + // fallback to german + $result = $app['translator']->trans('key1', array(), null, 'ru'); + $this->assertEquals('The german translation', $result); + } + + public function testLocale() + { + $app = $this->getPreparedApp(); + $app->get('/', function () use ($app) { return $app['translator']->getLocale(); }); + $response = $app->handle(Request::create('/')); + $this->assertEquals('en', $response->getContent()); + + $app = $this->getPreparedApp(); + $app->get('/', function () use ($app) { return $app['translator']->getLocale(); }); + $request = Request::create('/'); + $request->setLocale('fr'); + $response = $app->handle($request); + $this->assertEquals('fr', $response->getContent()); + + $app = $this->getPreparedApp(); + $app->get('/{_locale}', function () use ($app) { return $app['translator']->getLocale(); }); + $response = $app->handle(Request::create('/es')); + $this->assertEquals('es', $response->getContent()); + } + + public function testLocaleInSubRequests() + { + $app = $this->getPreparedApp(); + $app->get('/embed/{_locale}', function () use ($app) { return $app['translator']->getLocale(); }); + $app->get('/{_locale}', function () use ($app) { + return $app['translator']->getLocale(). + $app->handle(Request::create('/embed/es'), HttpKernelInterface::SUB_REQUEST)->getContent(). + $app['translator']->getLocale(); + }); + $response = $app->handle(Request::create('/fr')); + $this->assertEquals('fresfr', $response->getContent()); + + $app = $this->getPreparedApp(); + $app->get('/embed', function () use ($app) { return $app['translator']->getLocale(); }); + $app->get('/{_locale}', function () use ($app) { + return $app['translator']->getLocale(). + $app->handle(Request::create('/embed'), HttpKernelInterface::SUB_REQUEST)->getContent(). + $app['translator']->getLocale(); + }); + $response = $app->handle(Request::create('/fr')); + // locale in sub-request must be "en" as this is the value if the sub-request is converted to an ESI + $this->assertEquals('frenfr', $response->getContent()); + } + + public function testLocaleWithBefore() + { + $app = $this->getPreparedApp(); + $app->before(function (Request $request) { $request->setLocale('fr'); }, Application::EARLY_EVENT); + $app->get('/embed', function () use ($app) { return $app['translator']->getLocale(); }); + $app->get('/', function () use ($app) { + return $app['translator']->getLocale(). + $app->handle(Request::create('/embed'), HttpKernelInterface::SUB_REQUEST)->getContent(). + $app['translator']->getLocale(); + }); + $response = $app->handle(Request::create('/')); + // locale in sub-request is "en" as the before filter is only executed for the main request + $this->assertEquals('frenfr', $response->getContent()); + } +} diff --git a/tests/Silex/Tests/Provider/TwigServiceProviderTest.php b/tests/Silex/Tests/Provider/TwigServiceProviderTest.php new file mode 100644 index 0000000..24fca01 --- /dev/null +++ b/tests/Silex/Tests/Provider/TwigServiceProviderTest.php @@ -0,0 +1,119 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider; + +use Silex\Application; +use Silex\Provider\CsrfServiceProvider; +use Silex\Provider\FormServiceProvider; +use Silex\Provider\TwigServiceProvider; +use Silex\Provider\AssetServiceProvider; +use Symfony\Component\HttpFoundation\Request; + +/** + * TwigProvider test cases. + * + * @author Igor Wiedler + */ +class TwigServiceProviderTest extends \PHPUnit_Framework_TestCase +{ + public function testRegisterAndRender() + { + $app = new Application(); + + $app->register(new TwigServiceProvider(), array( + 'twig.templates' => array('hello' => 'Hello {{ name }}!'), + )); + + $app->get('/hello/{name}', function ($name) use ($app) { + return $app['twig']->render('hello', array('name' => $name)); + }); + + $request = Request::create('/hello/john'); + $response = $app->handle($request); + $this->assertEquals('Hello john!', $response->getContent()); + } + + public function testLoaderPriority() + { + $app = new Application(); + $app->register(new TwigServiceProvider(), array( + 'twig.templates' => array('foo' => 'foo'), + )); + $loader = $this->getMock('\Twig_LoaderInterface'); + $loader->expects($this->never())->method('getSource'); + $app['twig.loader.filesystem'] = function ($app) use ($loader) { + return $loader; + }; + $this->assertEquals('foo', $app['twig.loader']->getSource('foo')); + } + + public function testHttpFoundationIntegration() + { + $app = new Application(); + $app['request_stack']->push(Request::create('/dir1/dir2/file')); + $app->register(new TwigServiceProvider(), array( + 'twig.templates' => array( + 'absolute' => '{{ absolute_url("foo.css") }}', + 'relative' => '{{ relative_path("/dir1/foo.css") }}', + ), + )); + + $this->assertEquals('http://localhost/dir1/dir2/foo.css', $app['twig']->render('absolute')); + $this->assertEquals('../foo.css', $app['twig']->render('relative')); + } + + public function testAssetIntegration() + { + $app = new Application(); + $app->register(new TwigServiceProvider(), array( + 'twig.templates' => array('hello' => '{{ asset("/foo.css") }}'), + )); + $app->register(new AssetServiceProvider(), array( + 'assets.version' => 1, + )); + + $this->assertEquals('/foo.css?1', $app['twig']->render('hello')); + } + + public function testGlobalVariable() + { + $app = new Application(); + $app['request_stack']->push(Request::create('/?name=Fabien')); + + $app->register(new TwigServiceProvider(), array( + 'twig.templates' => array('hello' => '{{ global.request.get("name") }}'), + )); + + $this->assertEquals('Fabien', $app['twig']->render('hello')); + } + + public function testFormFactory() + { + $app = new Application(); + $app->register(new FormServiceProvider()); + $app->register(new CsrfServiceProvider()); + $app->register(new TwigServiceProvider()); + + $this->assertInstanceOf('Twig_Environment', $app['twig'], 'Service twig is created successful.'); + $this->assertInstanceOf('Symfony\Bridge\Twig\Form\TwigRendererEngine', $app['twig.form.engine'], 'Service twig.form.engine is created successful.'); + $this->assertInstanceOf('Symfony\Bridge\Twig\Form\TwigRenderer', $app['twig.form.renderer'], 'Service twig.form.renderer is created successful.'); + } + + public function testFormWithoutCsrf() + { + $app = new Application(); + $app->register(new FormServiceProvider()); + $app->register(new TwigServiceProvider()); + + $this->assertInstanceOf('Twig_Environment', $app['twig']); + } +} diff --git a/tests/Silex/Tests/Provider/ValidatorServiceProviderTest.php b/tests/Silex/Tests/Provider/ValidatorServiceProviderTest.php new file mode 100644 index 0000000..a28ccf7 --- /dev/null +++ b/tests/Silex/Tests/Provider/ValidatorServiceProviderTest.php @@ -0,0 +1,194 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider; + +use Silex\Application; +use Silex\Provider\TranslationServiceProvider; +use Silex\Provider\ValidatorServiceProvider; +use Silex\Provider\FormServiceProvider; +use Symfony\Component\Translation\Exception\NotFoundResourceException; +use Symfony\Component\Validator\Constraints as Assert; +use Silex\Tests\Provider\ValidatorServiceProviderTest\Constraint\Custom; +use Silex\Tests\Provider\ValidatorServiceProviderTest\Constraint\CustomValidator; +use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * ValidatorServiceProvider. + * + * Javier Lopez + */ +class ValidatorServiceProviderTest extends \PHPUnit_Framework_TestCase +{ + public function testRegister() + { + $app = new Application(); + $app->register(new ValidatorServiceProvider()); + $app->register(new FormServiceProvider()); + + return $app; + } + + public function testRegisterWithCustomValidators() + { + $app = new Application(); + + $app['custom.validator'] = function () { + return new CustomValidator(); + }; + + $app->register(new ValidatorServiceProvider(), array( + 'validator.validator_service_ids' => array( + 'test.custom.validator' => 'custom.validator', + ), + )); + + return $app; + } + + /** + * @depends testRegisterWithCustomValidators + */ + public function testConstraintValidatorFactory($app) + { + $this->assertInstanceOf('Silex\Provider\Validator\ConstraintValidatorFactory', $app['validator.validator_factory']); + + $validator = $app['validator.validator_factory']->getInstance(new Custom()); + $this->assertInstanceOf('Silex\Tests\Provider\ValidatorServiceProviderTest\Constraint\CustomValidator', $validator); + } + + /** + * @depends testRegister + */ + public function testConstraintValidatorFactoryWithExpression($app) + { + $constraint = new Assert\Expression('true'); + $validator = $app['validator.validator_factory']->getInstance($constraint); + $this->assertInstanceOf('Symfony\Component\Validator\Constraints\ExpressionValidator', $validator); + } + + /** + * @depends testRegister + */ + public function testValidatorServiceIsAValidator($app) + { + $this->assertTrue($app['validator'] instanceof ValidatorInterface || $app['validator'] instanceof LegacyValidatorInterface ); + } + + /** + * @depends testRegister + * @dataProvider getTestValidatorConstraintProvider + */ + public function testValidatorConstraint($email, $isValid, $nbGlobalError, $nbEmailError, $app) + { + $constraints = new Assert\Collection(array( + 'email' => array( + new Assert\NotBlank(), + new Assert\Email(), + ), + )); + + $builder = $app['form.factory']->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', array(), array( + 'constraints' => $constraints, + )); + + $form = $builder + ->add('email', 'Symfony\Component\Form\Extension\Core\Type\EmailType', array('label' => 'Email')) + ->getForm() + ; + + $form->submit(array('email' => $email)); + + $this->assertEquals($isValid, $form->isValid()); + $this->assertEquals($nbGlobalError, count($form->getErrors())); + $this->assertEquals($nbEmailError, count($form->offsetGet('email')->getErrors())); + } + + public function testValidatorWillNotAddNonexistentTranslationFiles() + { + $app = new Application(array( + 'locale' => 'nonexistent', + )); + + $app->register(new ValidatorServiceProvider()); + $app->register(new TranslationServiceProvider(), array( + 'locale_fallbacks' => array(), + )); + + $app['validator']; + $translator = $app['translator']; + + try { + $translator->trans('test'); + } catch (NotFoundResourceException $e) { + $this->fail('Validator should not add a translation resource that does not exist'); + } + } + + public function getTestValidatorConstraintProvider() + { + // Email, form is valid, nb global error, nb email error + return array( + array('', false, 0, 1), + array('not an email', false, 0, 1), + array('email@sample.com', true, 0, 0), + ); + } + + /** + * @dataProvider getAddResourceData + */ + public function testAddResource($registerValidatorFirst) + { + $app = new Application(); + $app['locale'] = 'fr'; + + $app->register(new ValidatorServiceProvider()); + $app->register(new TranslationServiceProvider()); + $app['translator'] = $app->extend('translator', function ($translator, $app) { + $translator->addResource('array', array('This value should not be blank.' => 'Pas vide'), 'fr', 'validators'); + + return $translator; + }); + + if ($registerValidatorFirst) { + $app['validator']; + } + + $this->assertEquals('Pas vide', $app['translator']->trans('This value should not be blank.', array(), 'validators', 'fr')); + } + + public function getAddResourceData() + { + return array(array(false), array(true)); + } + + public function testAddResourceAlternate() + { + $app = new Application(); + $app['locale'] = 'fr'; + + $app->register(new ValidatorServiceProvider()); + $app->register(new TranslationServiceProvider()); + $app->factory($app->extend('translator.resources', function ($resources, $app) { + $resources = array_merge($resources, array( + array('array', array('This value should not be blank.' => 'Pas vide'), 'fr', 'validators'), + )); + + return $resources; + })); + + $app['validator']; + + $this->assertEquals('Pas vide', $app['translator']->trans('This value should not be blank.', array(), 'validators', 'fr')); + } +} diff --git a/tests/Silex/Tests/Provider/ValidatorServiceProviderTest/Constraint/Custom.php b/tests/Silex/Tests/Provider/ValidatorServiceProviderTest/Constraint/Custom.php new file mode 100644 index 0000000..bef911a --- /dev/null +++ b/tests/Silex/Tests/Provider/ValidatorServiceProviderTest/Constraint/Custom.php @@ -0,0 +1,29 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider\ValidatorServiceProviderTest\Constraint; + +use Symfony\Component\Validator\Constraint; + +/** + * @author Alex Kalyvitis + */ +class Custom extends Constraint +{ + public $message = 'This field must be ...'; + public $table; + public $field; + + public function validatedBy() + { + return 'test.custom.validator'; + } +} diff --git a/tests/Silex/Tests/Provider/ValidatorServiceProviderTest/Constraint/CustomValidator.php b/tests/Silex/Tests/Provider/ValidatorServiceProviderTest/Constraint/CustomValidator.php new file mode 100644 index 0000000..856927d --- /dev/null +++ b/tests/Silex/Tests/Provider/ValidatorServiceProviderTest/Constraint/CustomValidator.php @@ -0,0 +1,32 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Provider\ValidatorServiceProviderTest\Constraint; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; + +/** + * @author Alex Kalyvitis + */ +class CustomValidator extends ConstraintValidator +{ + public function isValid($value, Constraint $constraint) + { + // Validate... + return true; + } + + public function validate($value, Constraint $constraint) + { + return $this->isValid($value, $constraint); + } +} diff --git a/tests/Silex/Tests/Route/SecurityRoute.php b/tests/Silex/Tests/Route/SecurityRoute.php new file mode 100644 index 0000000..4823719 --- /dev/null +++ b/tests/Silex/Tests/Route/SecurityRoute.php @@ -0,0 +1,19 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Route; + +use Silex\Route; + +class SecurityRoute extends Route +{ + use Route\SecurityTrait; +} diff --git a/tests/Silex/Tests/Route/SecurityTraitTest.php b/tests/Silex/Tests/Route/SecurityTraitTest.php new file mode 100644 index 0000000..352a77b --- /dev/null +++ b/tests/Silex/Tests/Route/SecurityTraitTest.php @@ -0,0 +1,85 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests\Route; + +use Silex\Application; +use Silex\Provider\SecurityServiceProvider; +use Symfony\Component\HttpFoundation\Request; + +/** + * SecurityTrait test cases. + * + * @author Fabien Potencier + */ +class SecurityTraitTest extends \PHPUnit_Framework_TestCase +{ + public function testSecureWithNoAuthenticatedUser() + { + $app = $this->createApplication(); + + $app->get('/', function () { return 'foo'; }) + ->secure('ROLE_ADMIN') + ; + + $request = Request::create('/'); + $response = $app->handle($request); + $this->assertEquals(401, $response->getStatusCode()); + } + + public function testSecureWithAuthorizedRoles() + { + $app = $this->createApplication(); + + $app->get('/', function () { return 'foo'; }) + ->secure('ROLE_ADMIN') + ; + + $request = Request::create('/'); + $request->headers->set('PHP_AUTH_USER', 'fabien'); + $request->headers->set('PHP_AUTH_PW', 'foo'); + $response = $app->handle($request); + $this->assertEquals(200, $response->getStatusCode()); + } + + public function testSecureWithUnauthorizedRoles() + { + $app = $this->createApplication(); + + $app->get('/', function () { return 'foo'; }) + ->secure('ROLE_SUPER_ADMIN') + ; + + $request = Request::create('/'); + $request->headers->set('PHP_AUTH_USER', 'fabien'); + $request->headers->set('PHP_AUTH_PW', 'foo'); + $response = $app->handle($request); + $this->assertEquals(403, $response->getStatusCode()); + } + + private function createApplication() + { + $app = new Application(); + $app['route_class'] = 'Silex\Tests\Route\SecurityRoute'; + $app->register(new SecurityServiceProvider(), array( + 'security.firewalls' => array( + 'default' => array( + 'http' => true, + 'users' => array( + 'fabien' => array('ROLE_ADMIN', '$2y$15$lzUNsTegNXvZW3qtfucV0erYBcEqWVeyOmjolB7R1uodsAVJ95vvu'), + ), + ), + ), + )); + + return $app; + } +} diff --git a/tests/Silex/Tests/RouterTest.php b/tests/Silex/Tests/RouterTest.php new file mode 100644 index 0000000..665891e --- /dev/null +++ b/tests/Silex/Tests/RouterTest.php @@ -0,0 +1,285 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests; + +use Silex\Application; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\RedirectResponse; + +/** + * Router test cases. + * + * @author Igor Wiedler + */ +class RouterTest extends \PHPUnit_Framework_TestCase +{ + public function testMapRouting() + { + $app = new Application(); + + $app->match('/foo', function () { + return 'foo'; + }); + + $app->match('/bar', function () { + return 'bar'; + }); + + $app->match('/', function () { + return 'root'; + }); + + $this->checkRouteResponse($app, '/foo', 'foo'); + $this->checkRouteResponse($app, '/bar', 'bar'); + $this->checkRouteResponse($app, '/', 'root'); + } + + public function testStatusCode() + { + $app = new Application(); + + $app->put('/created', function () { + return new Response('', 201); + }); + + $app->match('/forbidden', function () { + return new Response('', 403); + }); + + $app->match('/not_found', function () { + return new Response('', 404); + }); + + $request = Request::create('/created', 'put'); + $response = $app->handle($request); + $this->assertEquals(201, $response->getStatusCode()); + + $request = Request::create('/forbidden'); + $response = $app->handle($request); + $this->assertEquals(403, $response->getStatusCode()); + + $request = Request::create('/not_found'); + $response = $app->handle($request); + $this->assertEquals(404, $response->getStatusCode()); + } + + public function testRedirect() + { + $app = new Application(); + + $app->match('/redirect', function () { + return new RedirectResponse('/target'); + }); + + $app->match('/redirect2', function () use ($app) { + return $app->redirect('/target2'); + }); + + $request = Request::create('/redirect'); + $response = $app->handle($request); + $this->assertTrue($response->isRedirect('/target')); + + $request = Request::create('/redirect2'); + $response = $app->handle($request); + $this->assertTrue($response->isRedirect('/target2')); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ + public function testMissingRoute() + { + $app = new Application(); + unset($app['exception_handler']); + + $request = Request::create('/baz'); + $app->handle($request); + } + + public function testMethodRouting() + { + $app = new Application(); + + $app->match('/foo', function () { + return 'foo'; + }); + + $app->match('/bar', function () { + return 'bar'; + })->method('GET|POST'); + + $app->get('/resource', function () { + return 'get resource'; + }); + + $app->post('/resource', function () { + return 'post resource'; + }); + + $app->put('/resource', function () { + return 'put resource'; + }); + + $app->patch('/resource', function () { + return 'patch resource'; + }); + + $app->delete('/resource', function () { + return 'delete resource'; + }); + + $this->checkRouteResponse($app, '/foo', 'foo'); + $this->checkRouteResponse($app, '/bar', 'bar'); + $this->checkRouteResponse($app, '/bar', 'bar', 'post'); + $this->checkRouteResponse($app, '/resource', 'get resource'); + $this->checkRouteResponse($app, '/resource', 'post resource', 'post'); + $this->checkRouteResponse($app, '/resource', 'put resource', 'put'); + $this->checkRouteResponse($app, '/resource', 'patch resource', 'patch'); + $this->checkRouteResponse($app, '/resource', 'delete resource', 'delete'); + } + + public function testRequestShouldBeStoredRegardlessOfRouting() + { + $app = new Application(); + + $app->get('/foo', function (Request $request) use ($app) { + return new Response($request->getRequestUri()); + }); + + $app->error(function ($e, Request $request, $code) use ($app) { + return new Response($request->getRequestUri()); + }); + + foreach (array('/foo', '/bar') as $path) { + $request = Request::create($path); + $response = $app->handle($request); + $this->assertContains($path, $response->getContent()); + } + } + + public function testTrailingSlashBehavior() + { + $app = new Application(); + + $app->get('/foo/', function () use ($app) { + return new Response('ok'); + }); + + $request = Request::create('/foo'); + $response = $app->handle($request); + + $this->assertEquals(301, $response->getStatusCode()); + $this->assertEquals('/foo/', $response->getTargetUrl()); + } + + public function testHostSpecification() + { + $route = new \Silex\Route(); + + $this->assertSame($route, $route->host('{locale}.example.com')); + $this->assertEquals('{locale}.example.com', $route->getHost()); + } + + public function testRequireHttpRedirect() + { + $app = new Application(); + + $app->match('/secured', function () { + return 'secured content'; + }) + ->requireHttp(); + + $request = Request::create('https://example.com/secured'); + $response = $app->handle($request); + $this->assertTrue($response->isRedirect('http://example.com/secured')); + } + + public function testRequireHttpsRedirect() + { + $app = new Application(); + + $app->match('/secured', function () { + return 'secured content'; + }) + ->requireHttps(); + + $request = Request::create('http://example.com/secured'); + $response = $app->handle($request); + $this->assertTrue($response->isRedirect('https://example.com/secured')); + } + + public function testRequireHttpsRedirectIncludesQueryString() + { + $app = new Application(); + + $app->match('/secured', function () { + return 'secured content'; + }) + ->requireHttps(); + + $request = Request::create('http://example.com/secured?query=string'); + $response = $app->handle($request); + $this->assertTrue($response->isRedirect('https://example.com/secured?query=string')); + } + + public function testConditionOnRoute() + { + $app = new Application(); + $app->match('/secured', function () { + return 'secured content'; + }) + ->when('request.isSecure() == true'); + + $request = Request::create('http://example.com/secured'); + $response = $app->handle($request); + $this->assertEquals(404, $response->getStatusCode()); + } + + public function testClassNameControllerSyntax() + { + $app = new Application(); + + $app->get('/foo', 'Silex\Tests\MyController::getFoo'); + + $this->checkRouteResponse($app, '/foo', 'foo'); + } + + public function testClassNameControllerSyntaxWithStaticMethod() + { + $app = new Application(); + + $app->get('/bar', 'Silex\Tests\MyController::getBar'); + + $this->checkRouteResponse($app, '/bar', 'bar'); + } + + protected function checkRouteResponse(Application $app, $path, $expectedContent, $method = 'get', $message = null) + { + $request = Request::create($path, $method); + $response = $app->handle($request); + $this->assertEquals($expectedContent, $response->getContent(), $message); + } +} + +class MyController +{ + public function getFoo() + { + return 'foo'; + } + + public static function getBar() + { + return 'bar'; + } +} diff --git a/tests/Silex/Tests/ServiceControllerResolverRouterTest.php b/tests/Silex/Tests/ServiceControllerResolverRouterTest.php new file mode 100644 index 0000000..4bc88a4 --- /dev/null +++ b/tests/Silex/Tests/ServiceControllerResolverRouterTest.php @@ -0,0 +1,43 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests; + +use Silex\Application; +use Silex\Provider\ServiceControllerServiceProvider; +use Symfony\Component\HttpFoundation\Request; + +/** + * Router test cases, using the ServiceControllerResolver. + */ +class ServiceControllerResolverRouterTest extends RouterTest +{ + public function testServiceNameControllerSyntax() + { + $app = new Application(); + $app->register(new ServiceControllerServiceProvider()); + + $app['service_name'] = function () { + return new MyController(); + }; + + $app->get('/bar', 'service_name:getBar'); + + $this->checkRouteResponse($app, '/bar', 'bar'); + } + + protected function checkRouteResponse(Application $app, $path, $expectedContent, $method = 'get', $message = null) + { + $request = Request::create($path, $method); + $response = $app->handle($request); + $this->assertEquals($expectedContent, $response->getContent(), $message); + } +} diff --git a/tests/Silex/Tests/ServiceControllerResolverTest.php b/tests/Silex/Tests/ServiceControllerResolverTest.php new file mode 100644 index 0000000..f732c7d --- /dev/null +++ b/tests/Silex/Tests/ServiceControllerResolverTest.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Silex\Tests; + +use Silex\ServiceControllerResolver; +use Silex\Application; +use Symfony\Component\HttpFoundation\Request; + +/** + * Unit tests for ServiceControllerResolver, see ServiceControllerResolverRouterTest for some + * integration tests. + */ +class ServiceControllerResolverTest extends \PHPUnit_Framework_Testcase +{ + private $app; + private $mockCallbackResolver; + private $mockResolver; + private $resolver; + + public function setup() + { + $this->mockResolver = $this->getMockBuilder('Symfony\Component\HttpKernel\Controller\ControllerResolverInterface') + ->disableOriginalConstructor() + ->getMock(); + $this->mockCallbackResolver = $this->getMockBuilder('Silex\CallbackResolver') + ->disableOriginalConstructor() + ->getMock(); + + $this->app = new Application(); + $this->resolver = new ServiceControllerResolver($this->mockResolver, $this->mockCallbackResolver); + } + + public function testShouldResolveServiceController() + { + $this->mockCallbackResolver->expects($this->once()) + ->method('isValid') + ->will($this->returnValue(true)); + + $this->mockCallbackResolver->expects($this->once()) + ->method('convertCallback') + ->with('some_service:methodName') + ->will($this->returnValue(array('callback'))); + + $this->app['some_service'] = function () { return new \stdClass(); }; + + $req = Request::create('/'); + $req->attributes->set('_controller', 'some_service:methodName'); + + $this->assertEquals(array('callback'), $this->resolver->getController($req)); + } + + public function testShouldUnresolvedControllerNames() + { + $req = Request::create('/'); + $req->attributes->set('_controller', 'some_class::methodName'); + + $this->mockCallbackResolver->expects($this->once()) + ->method('isValid') + ->with('some_class::methodName') + ->will($this->returnValue(false)); + + $this->mockResolver->expects($this->once()) + ->method('getController') + ->with($req) + ->will($this->returnValue(123)); + + $this->assertEquals(123, $this->resolver->getController($req)); + } + + public function testShouldDelegateGetArguments() + { + $req = Request::create('/'); + $this->mockResolver->expects($this->once()) + ->method('getArguments') + ->with($req) + ->will($this->returnValue(123)); + + $this->assertEquals(123, $this->resolver->getArguments($req, function () {})); + } +} diff --git a/tests/Silex/Tests/StreamTest.php b/tests/Silex/Tests/StreamTest.php new file mode 100644 index 0000000..601f0e6 --- /dev/null +++ b/tests/Silex/Tests/StreamTest.php @@ -0,0 +1,52 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests; + +use Silex\Application; +use Symfony\Component\HttpFoundation\Request; + +/** + * Stream test cases. + * + * @author Igor Wiedler + */ +class StreamTest extends \PHPUnit_Framework_TestCase +{ + public function testStreamReturnsStreamingResponse() + { + $app = new Application(); + + $response = $app->stream(); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\StreamedResponse', $response); + $this->assertSame(false, $response->getContent()); + } + + public function testStreamActuallyStreams() + { + $i = 0; + + $stream = function () use (&$i) { + ++$i; + }; + + $app = new Application(); + $response = $app->stream($stream); + + $this->assertEquals(0, $i); + + $request = Request::create('/stream'); + $response->prepare($request); + $response->sendContent(); + + $this->assertEquals(1, $i); + } +} diff --git a/tests/Silex/Tests/WebTestCaseTest.php b/tests/Silex/Tests/WebTestCaseTest.php new file mode 100644 index 0000000..474ffc3 --- /dev/null +++ b/tests/Silex/Tests/WebTestCaseTest.php @@ -0,0 +1,78 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Silex\Tests; + +use Silex\Application; +use Silex\WebTestCase; +use Symfony\Component\HttpFoundation\Request; + +/** + * Functional test cases. + * + * @author Igor Wiedler + */ +class WebTestCaseTest extends WebTestCase +{ + public function createApplication() + { + $app = new Application(); + + $app->match('/hello', function () { + return 'world'; + }); + + $app->match('/html', function () { + return '

title

'; + }); + + $app->match('/server', function (Request $request) use ($app) { + $user = $request->server->get('PHP_AUTH_USER'); + $pass = $request->server->get('PHP_AUTH_PW'); + + return "

$user:$pass

"; + }); + + return $app; + } + + public function testGetHello() + { + $client = $this->createClient(); + + $client->request('GET', '/hello'); + $response = $client->getResponse(); + $this->assertTrue($response->isSuccessful()); + $this->assertEquals('world', $response->getContent()); + } + + public function testCrawlerFilter() + { + $client = $this->createClient(); + + $crawler = $client->request('GET', '/html'); + $this->assertEquals('title', $crawler->filter('h1')->text()); + } + + public function testServerVariables() + { + $user = 'klaus'; + $pass = '123456'; + + $client = $this->createClient(array( + 'PHP_AUTH_USER' => $user, + 'PHP_AUTH_PW' => $pass, + )); + + $crawler = $client->request('GET', '/server'); + $this->assertEquals("$user:$pass", $crawler->filter('h1')->text()); + } +} diff --git a/web/index.php b/web/index.php new file mode 100644 index 0000000..11d54db --- /dev/null +++ b/web/index.php @@ -0,0 +1,11 @@ +get('/', function () { + return 'Hello!'; +}); + +$app->run(); diff --git a/web/index.php~ b/web/index.php~ new file mode 100644 index 0000000..683c610 --- /dev/null +++ b/web/index.php~ @@ -0,0 +1,11 @@ +get('/hello', function () { + return 'Hello!'; +}); + +$app->run();