├── .editorconfig ├── .env ├── .env.test ├── .gitignore ├── .php_cs.dist ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── appveyor.yml ├── assets ├── js │ ├── admin.js │ ├── app.js │ ├── doclinks.js │ ├── highlight.js │ ├── jquery.instantSearch.js │ ├── login.js │ └── search.js └── scss │ ├── admin.scss │ ├── app.scss │ └── bootstrap-tagsinput.scss ├── bin ├── console └── phpunit ├── codeception.yml ├── composer.json ├── composer.lock ├── config ├── bootstrap.php ├── bundles.php ├── packages │ ├── assets.yaml │ ├── cache.yaml │ ├── dev │ │ ├── debug.yaml │ │ ├── monolog.yaml │ │ ├── routing.yaml │ │ ├── security_checker.yaml │ │ ├── swiftmailer.yaml │ │ └── web_profiler.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── framework.yaml │ ├── html_sanitizer.yaml │ ├── prod │ │ ├── doctrine.yaml │ │ └── monolog.yaml │ ├── routing.yaml │ ├── security.yaml │ ├── security_checker.yaml │ ├── sensio_framework_extra.yaml │ ├── swiftmailer.yaml │ ├── test │ │ ├── dama_doctrine_test_bundle.yaml │ │ ├── framework.yaml │ │ ├── monolog.yaml │ │ ├── routing.yaml │ │ ├── security.yaml │ │ ├── swiftmailer.yaml │ │ └── web_profiler.yaml │ ├── translation.yaml │ ├── twig.yaml │ ├── twig_extensions.yaml │ ├── validator.yaml │ └── webpack_encore.yaml ├── routes.yaml ├── routes │ ├── annotations.yaml │ └── dev │ │ ├── twig.yaml │ │ └── web_profiler.yaml └── services.yaml ├── data ├── database.sqlite └── database_test.sqlite ├── docker-compose.yml ├── package.json ├── phpunit.xml.dist ├── public ├── apple-touch-icon.png ├── build │ ├── 0.js │ ├── 1.js │ ├── 2.js │ ├── admin.css │ ├── admin.js │ ├── app.css │ ├── app.js │ ├── entrypoints.json │ ├── fonts │ │ ├── fa-brands-400.14c590d1.eot │ │ ├── fa-brands-400.3e1b2a65.woff2 │ │ ├── fa-brands-400.5e8aa9ea.ttf │ │ ├── fa-brands-400.df02c782.woff │ │ ├── fa-regular-400.285a9d2a.ttf │ │ ├── fa-regular-400.5623624d.woff │ │ ├── fa-regular-400.aa66d0e0.eot │ │ ├── fa-regular-400.ac21cac3.woff2 │ │ ├── fa-solid-900.3ded831d.woff │ │ ├── fa-solid-900.42e1fbd2.eot │ │ ├── fa-solid-900.896e20e2.ttf │ │ ├── fa-solid-900.d6d8d5da.woff2 │ │ ├── lato-bold-italic.0b6bb672.woff2 │ │ ├── lato-bold-italic.9c7e4e9e.woff │ │ ├── lato-bold.cccb8974.woff2 │ │ ├── lato-bold.d878b6c2.woff │ │ ├── lato-normal-italic.4eb103b4.woff2 │ │ ├── lato-normal-italic.f28f2d64.woff │ │ ├── lato-normal.27bd77b9.woff │ │ └── lato-normal.bd03a2cc.woff2 │ ├── images │ │ ├── fa-brands-400.bfa9c38b.svg │ │ ├── fa-regular-400.95f13e0b.svg │ │ └── fa-solid-900.6ed5e3bc.svg │ ├── login.js │ ├── manifest.json │ ├── runtime.js │ └── search.js ├── favicon.ico ├── index.php └── robots.txt ├── src ├── Command │ ├── AddUserCommand.php │ ├── DeleteUserCommand.php │ └── ListUsersCommand.php ├── Controller │ ├── Admin │ │ └── BlogController.php │ ├── BlogController.php │ ├── DomainController.php │ ├── SecurityController.php │ └── UserController.php ├── DataFixtures │ └── AppFixtures.php ├── Entity │ ├── Comment.php │ ├── Post.php │ ├── Tag.php │ └── User.php ├── EventSubscriber │ ├── CheckRequirementsSubscriber.php │ ├── CommentNotificationSubscriber.php │ ├── ControllerSubscriber.php │ └── RedirectToPreferredLocaleSubscriber.php ├── Events │ └── CommentCreatedEvent.php ├── Form │ ├── CommentType.php │ ├── DataTransformer │ │ └── TagArrayToStringTransformer.php │ ├── PostType.php │ ├── Type │ │ ├── ChangePasswordType.php │ │ ├── DateTimePickerType.php │ │ └── TagsInputType.php │ └── UserType.php ├── Kernel.php ├── Migrations │ └── .gitignore ├── Pagination │ └── Paginator.php ├── Repository │ ├── PostRepository.php │ ├── TagRepository.php │ └── UserRepository.php ├── Security │ └── PostVoter.php ├── Twig │ ├── AppExtension.php │ └── SourceCodeExtension.php └── Utils │ ├── Markdown.php │ ├── MomentFormatConverter.php │ ├── Slugger.php │ └── Validator.php ├── symfony.lock ├── templates ├── admin │ ├── blog │ │ ├── _delete_form.html.twig │ │ ├── _form.html.twig │ │ ├── edit.html.twig │ │ ├── index.html.twig │ │ ├── new.html.twig │ │ └── show.html.twig │ └── layout.html.twig ├── base.html.twig ├── blog │ ├── _comment_form.html.twig │ ├── _delete_post_confirmation.html.twig │ ├── _post_tags.html.twig │ ├── _rss.html.twig │ ├── about.html.twig │ ├── comment_form_error.html.twig │ ├── index.html.twig │ ├── index.xml.twig │ ├── post_show.html.twig │ └── search.html.twig ├── bundles │ └── TwigBundle │ │ └── Exception │ │ ├── error.html.twig │ │ ├── error403.html.twig │ │ ├── error404.html.twig │ │ └── error500.html.twig ├── debug │ └── source_code.html.twig ├── default │ ├── _flash_messages.html.twig │ └── homepage.html.twig ├── form │ ├── fields.html.twig │ └── layout.html.twig ├── security │ └── login.html.twig └── user │ ├── change_password.html.twig │ └── edit.html.twig ├── tests ├── _data │ └── .gitkeep ├── _output │ └── .gitignore ├── _support │ ├── AcceptanceTester.php │ ├── FunctionalTester.php │ ├── Helper │ │ ├── Acceptance.php │ │ ├── Functional.php │ │ └── Unit.php │ ├── UnitTester.php │ └── _generated │ │ └── .gitignore ├── acceptance.suite.yml ├── acceptance │ ├── .gitkeep │ ├── BlogCept.php │ └── LoginCept.php ├── functional.suite.yml ├── functional │ ├── .gitkeep │ ├── AuthCest.php │ ├── BlogCest.php │ ├── CommentCest.php │ ├── PostCrudCest.php │ ├── PublicUrlsCept.php │ ├── SecureUrlsCept.php │ ├── ServiceContainerCest.php │ ├── SubdomainIndexCept.php │ └── UnknownSubdomainThrowsExceptionCept.php ├── unit.suite.yml └── unit │ ├── .gitkeep │ ├── Form │ └── DataTransformer │ │ └── TagArrayToStringTransformerTest.php │ └── Utils │ ├── SluggerTest.php │ └── ValidatorTest.php ├── translations ├── messages+intl-icu.bg.xlf ├── messages+intl-icu.ca.xlf ├── messages+intl-icu.cs.xlf ├── messages+intl-icu.de.xlf ├── messages+intl-icu.en.xlf ├── messages+intl-icu.es.xlf ├── messages+intl-icu.fr.xlf ├── messages+intl-icu.hr.xlf ├── messages+intl-icu.id.xlf ├── messages+intl-icu.it.xlf ├── messages+intl-icu.ja.xlf ├── messages+intl-icu.lt.xlf ├── messages+intl-icu.nl.xlf ├── messages+intl-icu.pl.xlf ├── messages+intl-icu.pt_BR.xlf ├── messages+intl-icu.ro.xlf ├── messages+intl-icu.ru.xlf ├── messages+intl-icu.sl.xlf ├── messages+intl-icu.tr.xlf ├── messages+intl-icu.uk.xlf ├── messages+intl-icu.zh_CN.xlf ├── validators+intl-icu.bg.xlf ├── validators+intl-icu.ca.xlf ├── validators+intl-icu.cs.xlf ├── validators+intl-icu.de.xlf ├── validators+intl-icu.en.xlf ├── validators+intl-icu.es.xlf ├── validators+intl-icu.fr.xlf ├── validators+intl-icu.hr.xlf ├── validators+intl-icu.id.xlf ├── validators+intl-icu.it.xlf ├── validators+intl-icu.ja.xlf ├── validators+intl-icu.lt.xlf ├── validators+intl-icu.nl.xlf ├── validators+intl-icu.pl.xlf ├── validators+intl-icu.pt_BR.xlf ├── validators+intl-icu.ro.xlf ├── validators+intl-icu.ru.xlf ├── validators+intl-icu.sl.xlf ├── validators+intl-icu.tr.xlf ├── validators+intl-icu.uk.xlf └── validators+intl-icu.zh_CN.xlf ├── var ├── data │ ├── blog.sqlite │ └── blog_test.sqlite ├── log │ └── .gitkeep └── sessions │ └── .gitkeep ├── webpack.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | ; top-most EditorConfig file 2 | root = true 3 | 4 | ; Unix-style newlines 5 | [*] 6 | end_of_line = LF 7 | 8 | [*.php] 9 | indent_style = space 10 | indent_size = 4 11 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # This file is a "template" of which env vars needs to be defined in your configuration or in an .env file 2 | # Set variables here that may be different on each deployment target of the app, e.g. development, staging, production. 3 | # https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration 4 | 5 | ###> symfony/framework-bundle ### 6 | APP_ENV=dev 7 | APP_DEBUG=1 8 | APP_SECRET=67d829bf61dc5f87a73fd814e2c9f629 9 | ###< symfony/framework-bundle ### 10 | 11 | ###> doctrine/doctrine-bundle ### 12 | # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url 13 | # For a MySQL database, use: "mysql://db_user:db_password@127.0.0.1:3306/db_name" 14 | # Configure your db driver and server_version in config/packages/doctrine.yaml 15 | DATABASE_URL=sqlite:///%kernel.project_dir%/data/database.sqlite 16 | ###< doctrine/doctrine-bundle ### 17 | 18 | ###> symfony/swiftmailer-bundle ### 19 | # For Gmail as a transport, use: "gmail://username:password@localhost" 20 | # For a generic SMTP server, use: "smtp://localhost:25?encryption=&auth_mode=" 21 | # Delivery is disabled by default via "null://localhost" 22 | MAILER_URL=null://localhost 23 | ###< symfony/swiftmailer-bundle ### 24 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # This file will be loaded after .env file and override environment 2 | # variables for your test environment. 3 | DATABASE_URL=sqlite:///%kernel.project_dir%/data/database_test.sqlite 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /node_modules/ 3 | /var/* 4 | !/var/cache 5 | /var/cache/* 6 | !var/cache/.gitkeep 7 | !/var/data 8 | !/var/log 9 | /var/log/* 10 | !var/log/.gitkeep 11 | !/var/sessions 12 | /var/sessions/* 13 | !var/sessions/.gitkeep 14 | !var/SymfonyRequirements.php 15 | /public/build/fonts/glyphicons-* 16 | /public/build/images/glyphicons-* 17 | 18 | ###> symfony/framework-bundle ### 19 | /.env.local 20 | /.env.*.local 21 | /public/bundles/ 22 | /var/ 23 | /vendor/ 24 | ###< symfony/framework-bundle ### 25 | 26 | ###> symfony/phpunit-bridge ### 27 | .phpunit 28 | /phpunit.xml 29 | ###< symfony/phpunit-bridge ### 30 | 31 | ###> symfony/web-server-bundle ### 32 | /.web-server-pid 33 | ###< symfony/web-server-bundle ### 34 | 35 | ###> friendsofphp/php-cs-fixer ### 36 | /.php_cs 37 | /.php_cs.cache 38 | ###< friendsofphp/php-cs-fixer ### 39 | 40 | ###> symfony/webpack-encore-bundle ### 41 | /node_modules/ 42 | npm-debug.log 43 | yarn-error.log 44 | ###< symfony/webpack-encore-bundle ### 45 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | 7 | 8 | For the full copyright and license information, please view the LICENSE 9 | file that was distributed with this source code. 10 | COMMENT; 11 | 12 | $finder = PhpCsFixer\Finder::create() 13 | ->in(__DIR__) 14 | ->exclude('config') 15 | ->exclude('var') 16 | ->exclude('public/bundles') 17 | ->exclude('public/build') 18 | // exclude files generated by Symfony Flex recipes 19 | ->notPath('bin/console') 20 | ->notPath('public/index.php') 21 | ->exclude('tests') // Codeception & Symfony coding standards are different 22 | ; 23 | 24 | return PhpCsFixer\Config::create() 25 | ->setRiskyAllowed(true) 26 | ->setRules([ 27 | '@Symfony' => true, 28 | '@Symfony:risky' => true, 29 | 'array_syntax' => ['syntax' => 'short'], 30 | 'header_comment' => ['header' => $fileHeaderComment, 'separate' => 'both'], 31 | 'linebreak_after_opening_tag' => true, 32 | 'mb_str_functions' => true, 33 | 'no_php4_constructor' => true, 34 | 'no_superfluous_phpdoc_tags' => true, 35 | 'no_unreachable_default_argument_value' => true, 36 | 'no_useless_else' => true, 37 | 'no_useless_return' => true, 38 | 'ordered_imports' => true, 39 | 'php_unit_strict' => true, 40 | 'phpdoc_order' => true, 41 | 'semicolon_after_instruction' => true, 42 | 'strict_comparison' => true, 43 | 'strict_param' => true, 44 | ]) 45 | ->setFinder($finder) 46 | ->setCacheFile(__DIR__.'/var/.php_cs.cache') 47 | ; 48 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | sudo: false 3 | 4 | cache: 5 | yarn: true 6 | directories: 7 | - $HOME/.composer/cache/files 8 | - ./bin/.phpunit 9 | 10 | env: 11 | global: 12 | - SYMFONY_DEPRECATIONS_HELPER=weak 13 | 14 | matrix: 15 | fast_finish: true 16 | include: 17 | - php: 7.2 18 | - php: 7.3 19 | # 'php: nightly' is PHP 8.0 20 | - php: 7.4snapshot 21 | allow_failures: 22 | - php: 7.4snapshot 23 | 24 | before_install: 25 | - '[[ "$TRAVIS_PHP_VERSION" == "7.4snapshot" ]] || phpenv config-rm xdebug.ini' 26 | - composer self-update 27 | 28 | install: 29 | - COMPOSER_MEMORY_LIMIT=-1 composer update 30 | - ./bin/console server:start 31 | - ./vendor/bin/codecept build 32 | 33 | script: 34 | - ./vendor/bin/codecept run 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | The Symfony Demo application is an open source project. Contributions made by 5 | the community are welcome. Send us your ideas, code reviews, pull requests and 6 | feature requests to help us improve this project. All contributions must follow 7 | the [usual Symfony contribution requirements](https://symfony.com/doc/current/contributing/index.html). 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2019 Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Symfony Demo Application 2 | ======================== 3 | 4 | We use official "Symfony Demo Application" to demonstrate basics of Codeception functional tests. Check out `tests` directory to see functional and unit tests included. We use [Symfony](https://codeception.com/docs/modules/Symfony) and [Doctrine2](http://codeception.com/docs/modules/Doctrine2) modules for zero-configuration Symfony testing. 5 | 6 | ## Run Codeception Tests 7 | 8 | ``` 9 | composer install -n 10 | php bin/console doctrine:fixtures:load -n --env test 11 | php bin/codecept run 12 | ``` 13 | 14 | Below goes official README of Symfony Demo Application: 15 | 16 | --- 17 | 18 | The "Symfony Demo Application" is a reference application created to show how 19 | to develop applications following the [Symfony Best Practices][1]. 20 | 21 | Requirements 22 | ------------ 23 | 24 | * PHP 7.1.3 or higher; 25 | * PDO-SQLite PHP extension enabled; 26 | * and the [usual Symfony application requirements][2]. 27 | 28 | Installation 29 | ------------ 30 | 31 | Install the [Symfony client][4] binary and run this command: 32 | 33 | ```bash 34 | $ symfony new --demo my_project 35 | ``` 36 | 37 | Alternatively, you can use Composer: 38 | 39 | ```bash 40 | $ composer create-project symfony/symfony-demo my_project 41 | ``` 42 | 43 | Usage 44 | ----- 45 | 46 | There's no need to configure anything to run the application. If you have 47 | installed the [Symfony client][4] binary, run this command to run the built-in 48 | web server and access the application in your browser at : 49 | 50 | ```bash 51 | $ cd my_project/ 52 | $ symfony serve 53 | ``` 54 | 55 | If you don't have the Symfony client installed, run `php bin/console server:run`. 56 | Alternatively, you can [configure a web server][3] like Nginx or Apache to run 57 | the application. 58 | 59 | Tests 60 | ----- 61 | 62 | Execute this command to run tests: 63 | 64 | ```bash 65 | $ cd my_project/ 66 | $ ./bin/phpunit 67 | ``` 68 | 69 | [1]: https://symfony.com/doc/current/best_practices/index.html 70 | [2]: https://symfony.com/doc/current/reference/requirements.html 71 | [3]: https://symfony.com/doc/current/cookbook/configuration/web_server_configuration.html 72 | [4]: https://symfony.com/download 73 | [5]: https://github.com/symfony/webpack-encore 74 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: false 2 | clone_depth: 1 3 | clone_folder: c:\projects\symfony-demo 4 | 5 | cache: 6 | - '%LOCALAPPDATA%\Composer\files' 7 | - '%LOCALAPPDATA%\SymfonyBridge\phpunit' 8 | - c:\projects\symfony-demo\composer.phar 9 | 10 | init: 11 | - SET PATH=c:\php;%PATH% 12 | - SET COMPOSER_NO_INTERACTION=1 13 | - SET SYMFONY_DEPRECATIONS_HELPER=strict 14 | - SET SYMFONY_PHPUNIT_DIR=%LOCALAPPDATA%\SymfonyBridge\phpunit 15 | - SET ANSICON=121x90 (121x90) 16 | - REG ADD "HKEY_CURRENT_USER\Software\Microsoft\Command Processor" /v DelayedExpansion /t REG_DWORD /d 1 /f 17 | 18 | install: 19 | - mkdir c:\php && cd c:\php 20 | - appveyor DownloadFile https://raw.githubusercontent.com/symfony/binary-utils/master/cacert.pem 21 | - appveyor DownloadFile https://github.com/symfony/binary-utils/releases/download/v0.1/php-7.1.3-Win32-VC14-x86.zip 22 | - 7z x php-7.1.3-Win32-VC14-x86.zip -y >nul 23 | - del /Q *.zip 24 | - cd ext 25 | - appveyor DownloadFile https://github.com/symfony/binary-utils/releases/download/v0.1/php_apcu-5.1.8-7.1-ts-vc14-x86.zip 26 | - 7z x php_apcu-5.1.8-7.1-ts-vc14-x86.zip -y >nul 27 | - del /Q *.zip 28 | - cd .. 29 | - copy /Y php.ini-development php.ini 30 | - echo max_execution_time=1200 >> php.ini 31 | - echo date.timezone="America/Los_Angeles" >> php.ini 32 | - echo extension_dir=ext >> php.ini 33 | - echo zend_extension=php_opcache.dll >> php.ini 34 | - echo opcache.enable_cli=1 >> php.ini 35 | - echo extension=php_openssl.dll >> php.ini 36 | - echo extension=php_apcu.dll >> php.ini 37 | - echo apc.enable_cli=1 >> php.ini 38 | - echo extension=php_intl.dll >> php.ini 39 | - echo extension=php_mbstring.dll >> php.ini 40 | - echo extension=php_fileinfo.dll >> php.ini 41 | - echo extension=php_pdo_sqlite.dll >> php.ini 42 | - echo extension=php_curl.dll >> php.ini 43 | - echo curl.cainfo=c:\php\cacert.pem >> php.ini 44 | - cd c:\projects\symfony-demo 45 | - IF NOT EXIST composer.phar (appveyor DownloadFile https://getcomposer.org/download/1.3.0/composer.phar) 46 | - php composer.phar self-update 47 | - IF %APPVEYOR_REPO_BRANCH%==master (SET COMPOSER_ROOT_VERSION=dev-master) ELSE (SET COMPOSER_ROOT_VERSION=%APPVEYOR_REPO_BRANCH%.x-dev) 48 | - php composer.phar update --no-progress --ansi 49 | - SET COMPOSER_ROOT_VERSION= 50 | - vendor\bin\simple-phpunit install 51 | 52 | test_script: 53 | - cd c:\projects\symfony-demo 54 | - vendor/bin/simple-phpunit 55 | -------------------------------------------------------------------------------- /assets/js/admin.js: -------------------------------------------------------------------------------- 1 | import '../scss/admin.scss'; 2 | import 'eonasdan-bootstrap-datetimepicker'; 3 | import 'typeahead.js'; 4 | import Bloodhound from "bloodhound-js"; 5 | import 'bootstrap-tagsinput'; 6 | 7 | $(function() { 8 | // Datetime picker initialization. 9 | // See http://eonasdan.github.io/bootstrap-datetimepicker/ 10 | $('[data-toggle="datetimepicker"]').datetimepicker({ 11 | icons: { 12 | time: 'fa fa-clock-o', 13 | date: 'fa fa-calendar', 14 | up: 'fa fa-chevron-up', 15 | down: 'fa fa-chevron-down', 16 | previous: 'fa fa-chevron-left', 17 | next: 'fa fa-chevron-right', 18 | today: 'fa fa-check-circle-o', 19 | clear: 'fa fa-trash', 20 | close: 'fa fa-remove' 21 | } 22 | }); 23 | 24 | // Bootstrap-tagsinput initialization 25 | // http://bootstrap-tagsinput.github.io/bootstrap-tagsinput/examples/ 26 | var $input = $('input[data-toggle="tagsinput"]'); 27 | if ($input.length) { 28 | var source = new Bloodhound({ 29 | local: $input.data('tags'), 30 | queryTokenizer: Bloodhound.tokenizers.whitespace, 31 | datumTokenizer: Bloodhound.tokenizers.whitespace 32 | }); 33 | source.initialize(); 34 | 35 | $input.tagsinput({ 36 | trimValue: true, 37 | focusClass: 'focus', 38 | typeaheadjs: { 39 | name: 'tags', 40 | source: source.ttAdapter() 41 | } 42 | }); 43 | } 44 | }); 45 | 46 | // Handling the modal confirmation message. 47 | $(document).on('submit', 'form[data-confirmation]', function (event) { 48 | var $form = $(this), 49 | $confirm = $('#confirmationModal'); 50 | 51 | if ($confirm.data('result') !== 'yes') { 52 | //cancel submit event 53 | event.preventDefault(); 54 | 55 | $confirm 56 | .off('click', '#btnYes') 57 | .on('click', '#btnYes', function () { 58 | $confirm.data('result', 'yes'); 59 | $form.find('input[type="submit"]').attr('disabled', 'disabled'); 60 | $form.submit(); 61 | }) 62 | .modal('show'); 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | import '../scss/app.scss'; 2 | 3 | // loads the Bootstrap jQuery plugins 4 | import 'bootstrap-sass/assets/javascripts/bootstrap/transition.js'; 5 | import 'bootstrap-sass/assets/javascripts/bootstrap/alert.js'; 6 | import 'bootstrap-sass/assets/javascripts/bootstrap/collapse.js'; 7 | import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown.js'; 8 | import 'bootstrap-sass/assets/javascripts/bootstrap/modal.js'; 9 | import 'jquery' 10 | 11 | // loads the code syntax highlighting library 12 | import './highlight.js'; 13 | 14 | // Creates links to the Symfony documentation 15 | import './doclinks.js'; 16 | -------------------------------------------------------------------------------- /assets/js/doclinks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Wraps some elements in anchor tags referencing to the Symfony documentation 4 | $(function() { 5 | var $modal = $('#sourceCodeModal'); 6 | var $controllerCode = $modal.find('code.php'); 7 | var $templateCode = $modal.find('code.twig'); 8 | 9 | function anchor(url, content) { 10 | return '' + content + ''; 11 | }; 12 | 13 | // Wraps links to the Symfony documentation 14 | $modal.find('.hljs-comment').each(function() { 15 | $(this).html($(this).html().replace(/https:\/\/symfony.com\/doc\/[\w/.#-]+/g, function(url) { 16 | return anchor(url, url); 17 | })); 18 | }); 19 | 20 | // Wraps Symfony's annotations 21 | var annotations = { 22 | '@Cache': 'https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/cache.html', 23 | '@Method': 'https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/routing.html#route-method', 24 | '@ParamConverter': 'https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html', 25 | '@Route': 'https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/routing.html#usage', 26 | '@Security': 'https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/security.html' 27 | }; 28 | 29 | $controllerCode.find('.hljs-doctag').each(function() { 30 | var annotation = $(this).text(); 31 | 32 | if (annotations[annotation]) { 33 | $(this).html(anchor(annotations[annotation], annotation)); 34 | } 35 | }); 36 | 37 | // Wraps Twig's tags 38 | $templateCode.find('.hljs-template-tag > .hljs-name').each(function() { 39 | var tag = $(this).text(); 40 | 41 | if ('else' === tag || tag.match(/^end/)) { 42 | return; 43 | } 44 | 45 | var url = 'https://twig.symfony.com/doc/2.x/tags/' + tag + '.html#' + tag; 46 | 47 | $(this).html(anchor(url, tag)); 48 | }); 49 | 50 | // Wraps Twig's functions 51 | $templateCode.find('.hljs-template-variable > .hljs-name').each(function() { 52 | var func = $(this).text(); 53 | 54 | var url = 'https://twig.symfony.com/doc/2.x/functions/' + func + '.html#' + func; 55 | 56 | $(this).html(anchor(url, func)); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /assets/js/highlight.js: -------------------------------------------------------------------------------- 1 | import hljs from 'highlight.js/lib/highlight'; 2 | import php from 'highlight.js/lib/languages/php'; 3 | import twig from 'highlight.js/lib/languages/twig'; 4 | 5 | hljs.registerLanguage('php', php); 6 | hljs.registerLanguage('twig', twig); 7 | 8 | hljs.initHighlightingOnLoad(); 9 | -------------------------------------------------------------------------------- /assets/js/login.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var usernameEl = $('#username'); 3 | var passwordEl = $('#password'); 4 | 5 | // in a real application, hardcoding the user/password would be idiotic 6 | // but for the demo application it's very convenient to do so 7 | if (!usernameEl.val() || 'jane_admin' === usernameEl.val()) { 8 | usernameEl.val('jane_admin'); 9 | passwordEl.val('kitten'); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /assets/js/search.js: -------------------------------------------------------------------------------- 1 | import './jquery.instantSearch.js'; 2 | 3 | $(function() { 4 | $('.search-field').instantSearch({ 5 | delay: 100, 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /assets/scss/admin.scss: -------------------------------------------------------------------------------- 1 | @import "~bootswatch/flatly/variables"; 2 | @import "~eonasdan-bootstrap-datetimepicker/src/sass/bootstrap-datetimepicker-build.scss"; 3 | @import "bootstrap-tagsinput.scss"; 4 | 5 | /* Page: 'Backend post index' 6 | ------------------------------------------------------------------------- */ 7 | body#admin_post_index .item-actions { 8 | white-space: nowrap 9 | } 10 | 11 | body#admin_post_index .item-actions a.btn + a.btn { 12 | margin-left: 4px 13 | } 14 | 15 | /* Page: 'Backend post show' 16 | ------------------------------------------------------------------------- */ 17 | body#admin_post_show .post-tags .label-default { 18 | background-color: #e9ecec; 19 | color: #6D8283; 20 | font-size: 16px; 21 | margin-right: 10px; 22 | padding: .4em 1em .5em; 23 | } 24 | body#admin_post_show .post-tags .label-default i { 25 | color: #95A6A7; 26 | } 27 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(['--env', '-e'], null, true)) { 19 | putenv('APP_ENV='.$_ENV['APP_ENV']); 20 | // force loading .env files when --env is defined 21 | $_SERVER['APP_ENV'] = null; 22 | } 23 | 24 | if ($input->hasParameterOption('--no-debug', true)) { 25 | putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); 26 | } 27 | 28 | require dirname(__DIR__).'/config/bootstrap.php'; 29 | 30 | if ($_SERVER['APP_DEBUG']) { 31 | umask(0000); 32 | 33 | if (class_exists(Debug::class)) { 34 | Debug::enable(); 35 | } 36 | } 37 | 38 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 39 | $application = new Application($kernel); 40 | $application->run($input); 41 | -------------------------------------------------------------------------------- /bin/phpunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | =1.2) 9 | if (is_array($env = @include dirname(__DIR__).'/.env.local.php')) { 10 | foreach ($env as $k => $v) { 11 | $_ENV[$k] = $_ENV[$k] ?? (isset($_SERVER[$k]) && 0 !== strpos($k, 'HTTP_') ? $_SERVER[$k] : $v); 12 | } 13 | } elseif (!class_exists(Dotenv::class)) { 14 | throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); 15 | } else { 16 | // load all the .env files 17 | (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env'); 18 | } 19 | 20 | $_SERVER += $_ENV; 21 | $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; 22 | $_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; 23 | $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; 24 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 6 | Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle::class => ['all' => true], 7 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 8 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], 9 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 10 | Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true], 11 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 12 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true], 13 | Symfony\Bundle\WebServerBundle\WebServerBundle::class => ['dev' => true], 14 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 15 | DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], 16 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 17 | Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], 18 | Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], 19 | HtmlSanitizer\Bundle\HtmlSanitizerBundle::class => ['all' => true], 20 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 21 | ]; 22 | -------------------------------------------------------------------------------- /config/packages/assets.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | assets: 3 | json_manifest_path: '%kernel.project_dir%/public/build/manifest.json' 4 | -------------------------------------------------------------------------------- /config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | # Put the unique name of your app here: the prefix seed 4 | # is used to compute stable namespaces for cache keys. 5 | #prefix_seed: your_vendor_name/app_name 6 | 7 | # The app cache caches to the filesystem by default. 8 | # Other options include: 9 | 10 | # Redis 11 | #app: cache.adapter.redis 12 | #default_redis_provider: redis://localhost 13 | 14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 15 | #app: cache.adapter.apcu 16 | 17 | # Namespaced pools use the above "app" backend by default 18 | #pools: 19 | #my.dedicated.cache: ~ 20 | -------------------------------------------------------------------------------- /config/packages/dev/debug.yaml: -------------------------------------------------------------------------------- 1 | debug: 2 | # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. 3 | # See the "server:dump" command to start a new server. 4 | dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" 5 | -------------------------------------------------------------------------------- /config/packages/dev/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: stream 5 | path: '%kernel.logs_dir%/%kernel.environment%.log' 6 | level: debug 7 | channels: ['!event'] 8 | # uncomment to get logging in your browser 9 | # you may have to allow bigger header sizes in your Web server configuration 10 | #firephp: 11 | # type: firephp 12 | # level: info 13 | #chromephp: 14 | # type: chromephp 15 | # level: info 16 | console: 17 | type: console 18 | process_psr_3_messages: false 19 | channels: ['!event', '!doctrine', '!console'] 20 | -------------------------------------------------------------------------------- /config/packages/dev/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /config/packages/dev/security_checker.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | SensioLabs\Security\SecurityChecker: 3 | public: false 4 | 5 | SensioLabs\Security\Command\SecurityCheckerCommand: 6 | arguments: ['@SensioLabs\Security\SecurityChecker'] 7 | tags: 8 | - { name: console.command } 9 | -------------------------------------------------------------------------------- /config/packages/dev/swiftmailer.yaml: -------------------------------------------------------------------------------- 1 | # See https://symfony.com/doc/current/email/dev_environment.html 2 | swiftmailer: 3 | # send all emails to a specific address 4 | #delivery_addresses: ['me@example.com'] 5 | -------------------------------------------------------------------------------- /config/packages/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: true 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { only_exceptions: false } 7 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | # Adds a fallback DATABASE_URL if the env var is not set. This allows you 3 | # to run cache:warmup even if your environment variables are not available 4 | # yet. You should not need to change this value. 5 | env(DATABASE_URL): '' 6 | 7 | doctrine: 8 | dbal: 9 | driver: 'pdo_sqlite' 10 | server_version: '3.15' 11 | charset: utf8mb4 12 | 13 | url: '%env(resolve:DATABASE_URL)%' 14 | orm: 15 | auto_generate_proxy_classes: '%kernel.debug%' 16 | naming_strategy: doctrine.orm.naming_strategy.underscore 17 | auto_mapping: true 18 | mappings: 19 | App: 20 | is_bundle: false 21 | type: annotation 22 | dir: '%kernel.project_dir%/src/Entity' 23 | prefix: 'App\Entity' 24 | alias: App 25 | -------------------------------------------------------------------------------- /config/packages/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | dir_name: '%kernel.project_dir%/src/Migrations' 3 | # namespace is arbitrary but should be different from App\Migrations 4 | # as migrations classes should NOT be autoloaded 5 | namespace: DoctrineMigrations 6 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: '%env(APP_SECRET)%' 3 | csrf_protection: true 4 | http_method_override: true 5 | trusted_hosts: ~ 6 | session: 7 | # With this config, PHP's native session handling is used 8 | handler_id: ~ 9 | cookie_secure: auto 10 | cookie_samesite: lax 11 | # When using the HTTP Cache, ESI allows to render page fragments separately 12 | # and with different cache configurations for each fragment 13 | # https://symfony.com/doc/current/book/http_cache.html#edge-side-includes 14 | esi: true 15 | fragments: true 16 | php_errors: 17 | log: true 18 | assets: 19 | json_manifest_path: '%kernel.project_dir%/public/build/manifest.json' 20 | cache: 21 | # this value is used as part of the "namespace" generated for the cache item keys 22 | # to avoid collisions when multiple apps share the same cache backend (e.g. a Redis server) 23 | # See https://symfony.com/doc/current/reference/configuration/framework.html#prefix-seed 24 | prefix_seed: symfony-demo 25 | # The 'ide' option turns all of the file paths in an exception page 26 | # into clickable links that open the given file using your favorite IDE. 27 | # When 'ide' is set to null the file is opened in your web browser. 28 | # See https://symfony.com/doc/current/reference/configuration/framework.html#ide 29 | ide: ~ 30 | validation: 31 | email_validation_mode: 'html5' 32 | enable_annotations: true 33 | -------------------------------------------------------------------------------- /config/packages/html_sanitizer.yaml: -------------------------------------------------------------------------------- 1 | html_sanitizer: 2 | default_sanitizer: 'default' 3 | sanitizers: 4 | default: 5 | # Read https://github.com/tgalopin/html-sanitizer/blob/master/docs/1-getting-started.md#extensions 6 | # to learn more about which extensions you would like to enable. 7 | extensions: 8 | - 'basic' 9 | - 'list' 10 | - 'table' 11 | - 'image' 12 | - 'code' 13 | 14 | # Read https://github.com/tgalopin/html-sanitizer/blob/master/docs/3-configuration-reference.md 15 | # to discover all the available options for each extension. 16 | -------------------------------------------------------------------------------- /config/packages/prod/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | orm: 3 | metadata_cache_driver: 4 | type: service 5 | id: doctrine.system_cache_provider 6 | query_cache_driver: 7 | type: service 8 | id: doctrine.system_cache_provider 9 | result_cache_driver: 10 | type: service 11 | id: doctrine.result_cache_provider 12 | 13 | services: 14 | doctrine.result_cache_provider: 15 | class: Symfony\Component\Cache\DoctrineProvider 16 | public: false 17 | arguments: 18 | - '@doctrine.result_cache_pool' 19 | doctrine.system_cache_provider: 20 | class: Symfony\Component\Cache\DoctrineProvider 21 | public: false 22 | arguments: 23 | - '@doctrine.system_cache_pool' 24 | 25 | framework: 26 | cache: 27 | pools: 28 | doctrine.result_cache_pool: 29 | adapter: cache.app 30 | doctrine.system_cache_pool: 31 | adapter: cache.system 32 | -------------------------------------------------------------------------------- /config/packages/prod/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: nested 7 | excluded_http_codes: [404] 8 | nested: 9 | type: stream 10 | path: "%kernel.logs_dir%/%kernel.environment%.log" 11 | level: debug 12 | console: 13 | type: console 14 | process_psr_3_messages: false 15 | channels: ["!event", "!doctrine"] 16 | deprecation: 17 | type: stream 18 | path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log" 19 | deprecation_filter: 20 | type: filter 21 | handler: deprecation 22 | max_level: info 23 | channels: ["php"] 24 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: ~ 4 | utf8: true 5 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | encoders: 3 | # Our user class and the algorithm we'll use to encode passwords 4 | # 'auto' means to let Symfony choose the best possible password hasher (Argon2 or Bcrypt) 5 | # https://symfony.com/doc/current/security.html#c-encoding-the-user-s-password 6 | App\Entity\User: 'auto' 7 | 8 | providers: 9 | # https://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded 10 | # In this example, users are stored via Doctrine in the database 11 | # To see the users at src/App/DataFixtures/ORM/LoadFixtures.php 12 | # To load users from somewhere else: https://symfony.com/doc/current/security/custom_provider.html 13 | database_users: 14 | entity: { class: App\Entity\User, property: username } 15 | 16 | # https://symfony.com/doc/current/security.html#initial-security-yml-setup-authentication 17 | firewalls: 18 | dev: 19 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 20 | security: false 21 | 22 | main: 23 | # this firewall applies to all URLs 24 | pattern: ^/ 25 | 26 | # but the firewall does not require login on every page 27 | # denying access is done in access_control or in your controllers 28 | anonymous: true 29 | 30 | # This allows the user to login by submitting a username and password 31 | # Reference: https://symfony.com/doc/current/security/form_login_setup.html 32 | form_login: 33 | # The route name that the login form submits to 34 | check_path: security_login 35 | # The name of the route where the login form lives 36 | # When the user tries to access a protected page, they are redirected here 37 | login_path: security_login 38 | # Secure the login form against CSRF 39 | # Reference: https://symfony.com/doc/current/security/csrf_in_login_form.html 40 | csrf_token_generator: security.csrf.token_manager 41 | # The page users are redirect to when there is no previous page stored in the 42 | # session (for example when the users access directly to the login page). 43 | default_target_path: blog_index 44 | 45 | logout: 46 | # The route name the user can go to in order to logout 47 | path: security_logout 48 | # The name of the route to redirect to after logging out 49 | target: homepage 50 | 51 | access_control: 52 | # this is a catch-all for the admin area 53 | # additional security lives in the controllers 54 | - { path: '^/(%app_locales%)/admin', roles: ROLE_ADMIN } 55 | 56 | role_hierarchy: 57 | ROLE_ADMIN: ROLE_USER 58 | -------------------------------------------------------------------------------- /config/packages/security_checker.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | SensioLabs\Security\SecurityChecker: 3 | public: false 4 | 5 | SensioLabs\Security\Command\SecurityCheckerCommand: 6 | arguments: ['@SensioLabs\Security\SecurityChecker'] 7 | public: false 8 | tags: 9 | - { name: console.command, command: 'security:check' } 10 | -------------------------------------------------------------------------------- /config/packages/sensio_framework_extra.yaml: -------------------------------------------------------------------------------- 1 | sensio_framework_extra: 2 | router: 3 | annotations: false 4 | -------------------------------------------------------------------------------- /config/packages/swiftmailer.yaml: -------------------------------------------------------------------------------- 1 | swiftmailer: 2 | url: '%env(MAILER_URL)%' 3 | spool: { type: 'memory' } 4 | -------------------------------------------------------------------------------- /config/packages/test/dama_doctrine_test_bundle.yaml: -------------------------------------------------------------------------------- 1 | dama_doctrine_test: 2 | enable_static_connection: true 3 | enable_static_meta_data_cache: true 4 | enable_static_query_cache: true 5 | -------------------------------------------------------------------------------- /config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_id: session.storage.mock_file 5 | -------------------------------------------------------------------------------- /config/packages/test/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: stream 5 | path: "%kernel.logs_dir%/%kernel.environment%.log" 6 | level: debug 7 | channels: ["!event"] 8 | -------------------------------------------------------------------------------- /config/packages/test/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /config/packages/test/security.yaml: -------------------------------------------------------------------------------- 1 | # this configuration simplifies testing URLs protected by the security mechanism 2 | # See https://symfony.com/doc/current/cookbook/testing/http_authentication.html 3 | security: 4 | firewalls: 5 | main: 6 | http_basic: ~ 7 | -------------------------------------------------------------------------------- /config/packages/test/swiftmailer.yaml: -------------------------------------------------------------------------------- 1 | swiftmailer: 2 | disable_delivery: true 3 | -------------------------------------------------------------------------------- /config/packages/test/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: false 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { collect: false } 7 | -------------------------------------------------------------------------------- /config/packages/translation.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | default_locale: '%locale%' 3 | translator: 4 | default_path: '%kernel.project_dir%/translations' 5 | fallbacks: 6 | - '%locale%' 7 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | debug: '%kernel.debug%' 3 | default_path: '%kernel.project_dir%/templates' 4 | strict_variables: '%kernel.debug%' 5 | form_themes: 6 | - 'form/layout.html.twig' 7 | - 'form/fields.html.twig' 8 | -------------------------------------------------------------------------------- /config/packages/twig_extensions.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | public: false 4 | autowire: true 5 | autoconfigure: true 6 | 7 | #Twig\Extensions\ArrayExtension: ~ 8 | #Twig\Extensions\DateExtension: ~ 9 | Twig\Extensions\IntlExtension: ~ # needed for the 'localizeddate' filter 10 | #Twig\Extensions\TextExtension: ~ 11 | -------------------------------------------------------------------------------- /config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | email_validation_mode: html5 4 | -------------------------------------------------------------------------------- /config/packages/webpack_encore.yaml: -------------------------------------------------------------------------------- 1 | webpack_encore: 2 | # The path where Encore is building the assets. 3 | # This should match Encore.setOutputPath() in webpack.config.js. 4 | output_path: '%kernel.project_dir%/public/build' 5 | -------------------------------------------------------------------------------- /config/routes.yaml: -------------------------------------------------------------------------------- 1 | # These lines define a route using YAML configuration. The controller used by 2 | # the route (FrameworkBundle:Template:template) is a convenient shortcut when 3 | # the template can be rendered without executing any logic in your own controller. 4 | # See https://symfony.com/doc/current/cookbook/templating/render_without_controller.html 5 | homepage: 6 | path: /{_locale} 7 | controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController::templateAction 8 | requirements: 9 | _locale: '%app_locales%' 10 | defaults: 11 | template: default/homepage.html.twig 12 | _locale: '%locale%' 13 | -------------------------------------------------------------------------------- /config/routes/annotations.yaml: -------------------------------------------------------------------------------- 1 | controllers: 2 | resource: '../src/Controller/' 3 | type: annotation 4 | prefix: /{_locale} 5 | requirements: 6 | _locale: '%app_locales%' 7 | defaults: 8 | _locale: '%locale%' 9 | -------------------------------------------------------------------------------- /config/routes/dev/twig.yaml: -------------------------------------------------------------------------------- 1 | _errors: 2 | resource: '@TwigBundle/Resources/config/routing/errors.xml' 3 | prefix: /_error 4 | -------------------------------------------------------------------------------- /config/routes/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler_wdt: 2 | resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' 3 | prefix: /_wdt 4 | 5 | web_profiler_profiler: 6 | resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' 7 | prefix: /_profiler 8 | -------------------------------------------------------------------------------- /config/services.yaml: -------------------------------------------------------------------------------- 1 | # Put parameters here that don't need to change on each machine where the app is deployed 2 | # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration 3 | parameters: 4 | locale: 'en' 5 | # This parameter defines the codes of the locales (languages) enabled in the application 6 | app_locales: en|fr|de|es|cs|nl|ru|uk|ro|pt_BR|pl|it|ja|id|ca|sl|hr|zh_CN|bg|tr|lt 7 | app.notifications.email_sender: anonymous@example.com 8 | 9 | services: 10 | # default configuration for services in *this* file 11 | _defaults: 12 | autowire: true # Automatically injects dependencies in your services. 13 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 14 | bind: # defines the scalar arguments once and apply them to any service defined/created in this file 15 | $locales: '%app_locales%' 16 | $defaultLocale: '%locale%' 17 | $emailSender: '%app.notifications.email_sender%' 18 | 19 | # makes classes in src/ available to be used as services 20 | # this creates a service per class whose id is the fully-qualified class name 21 | App\: 22 | resource: '../src/*' 23 | exclude: '../src/{Entity,Migrations,Tests,Kernel.php}' 24 | 25 | # controllers are imported separately to make sure services can be injected 26 | # as action arguments even if you don't extend any base controller class 27 | App\Controller\: 28 | resource: '../src/Controller' 29 | tags: ['controller.service_arguments'] 30 | 31 | # when the service definition only contains arguments, you can omit the 32 | # 'arguments' key and define the arguments just below the service class 33 | App\EventSubscriber\CommentNotificationSubscriber: 34 | $sender: '%app.notifications.email_sender%' 35 | 36 | App\DataFixtures\: 37 | resource: '../src/DataFixtures' 38 | tags: [doctrine.fixture.orm] 39 | -------------------------------------------------------------------------------- /data/database.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/data/database.sqlite -------------------------------------------------------------------------------- /data/database_test.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/data/database_test.sqlite -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | codeception: 4 | image: codeception/codeception 5 | depends_on: 6 | - firefox 7 | - web 8 | volumes: 9 | - ./src:/src 10 | - ./tests:/tests 11 | - ./codeception.yml:/codeception.yml 12 | web: 13 | image: php:7-apache 14 | depends_on: 15 | - db 16 | volumes: 17 | - .:/var/www/html 18 | db: 19 | image: percona:5.6 20 | ports: 21 | - '3306' 22 | firefox: 23 | image: selenium/standalone-firefox-debug:2.53.0 24 | ports: 25 | - '4444' 26 | - '5900' 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@fortawesome/fontawesome-free": "^5.8.1", 4 | "@symfony/webpack-encore": "^0.28.0", 5 | "bloodhound-js": "^1.2.3", 6 | "bootstrap-sass": "^3.3.7", 7 | "bootstrap-tagsinput": "^0.7.1", 8 | "bootswatch": "^3.3.7", 9 | "core-js": "^3.0.0", 10 | "eonasdan-bootstrap-datetimepicker": "^4.17.47", 11 | "highlight.js": "^9.12.0", 12 | "imports-loader": "^0.8.0", 13 | "jquery": "^3.3.1", 14 | "lato-font": "^3.0.0", 15 | "node-sass": "^4.9.3", 16 | "sass-loader": "^7.1.0", 17 | "typeahead.js": "^0.11.1" 18 | }, 19 | "license": "UNLICENSED", 20 | "private": true, 21 | "scripts": { 22 | "dev-server": "encore dev-server", 23 | "dev": "encore dev", 24 | "watch": "encore dev --watch", 25 | "build": "encore production --progress" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | tests/ 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/build/entrypoints.json: -------------------------------------------------------------------------------- 1 | { 2 | "entrypoints": { 3 | "app": { 4 | "js": [ 5 | "/build/runtime.js", 6 | "/build/0.js", 7 | "/build/1.js", 8 | "/build/app.js" 9 | ], 10 | "css": [ 11 | "/build/app.css" 12 | ] 13 | }, 14 | "login": { 15 | "js": [ 16 | "/build/runtime.js", 17 | "/build/0.js", 18 | "/build/login.js" 19 | ] 20 | }, 21 | "admin": { 22 | "js": [ 23 | "/build/runtime.js", 24 | "/build/0.js", 25 | "/build/2.js", 26 | "/build/admin.js" 27 | ], 28 | "css": [ 29 | "/build/admin.css" 30 | ] 31 | }, 32 | "search": { 33 | "js": [ 34 | "/build/runtime.js", 35 | "/build/0.js", 36 | "/build/1.js", 37 | "/build/search.js" 38 | ] 39 | } 40 | }, 41 | "integrity": { 42 | "/build/runtime.js": "sha384-/Zh0hTCgqMfXwZfeMIiueZzsKiHkvtqEOzKpZAuznIbgqg1DQpUQAwwNlO/9ToVH", 43 | "/build/0.js": "sha384-Dt6rncfcL40e91sPDLy/fZ8XgfH6ZZmLY1+ajXaPmGFGTb5XNtWS5jmf84NoZT/f", 44 | "/build/1.js": "sha384-bmKW9QGT6CjdvJr6hoYLCByhKzyjU+g+gUJ5SGOSxqUm4jzLuBhDpl0hImY/1R3M", 45 | "/build/app.js": "sha384-uCtNC3fXHx82+vdtTkHRXofGzNn6MJtN1ChaO2RaxA2IaRj/Ybj3QNldmJ6X0jI1", 46 | "/build/app.css": "sha384-NGTdnyfcvVpvCvvEryRps/xU6Mb67YeTf1pvFrUkd80uXKIuxzYmcXzvzttKu4d0", 47 | "/build/login.js": "sha384-w5qwGnj2mp4pazKe9KGCQU+FR1n/717WouCp594UxY7HHGLafcGrZ9mrFR68V6oe", 48 | "/build/2.js": "sha384-zrtPryvCde+sRepOgkuLELopii2NoIgS3rrkpCfY8e7nT359Sc0tXRG2QqsKLfEO", 49 | "/build/admin.js": "sha384-My30UK7GowsN0RikK0XGQk7gvdtxxZsz2f52md+YoV4YZu4yjMArjwO7ZwE7K4av", 50 | "/build/admin.css": "sha384-NMnc2b6jJOZO4QiUk4TgitF+ipl41B2+0TFmAwGqpQ6cinY6Q6mb1watPtAAWkAZ", 51 | "/build/search.js": "sha384-R/BW5h0YHjTYt/CVNZNOPDSuk0IQsvSSQZeP9hPpBtEkI4pBWMkBX4CAQnQQkFvt" 52 | } 53 | } -------------------------------------------------------------------------------- /public/build/fonts/fa-brands-400.14c590d1.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/fa-brands-400.14c590d1.eot -------------------------------------------------------------------------------- /public/build/fonts/fa-brands-400.3e1b2a65.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/fa-brands-400.3e1b2a65.woff2 -------------------------------------------------------------------------------- /public/build/fonts/fa-brands-400.5e8aa9ea.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/fa-brands-400.5e8aa9ea.ttf -------------------------------------------------------------------------------- /public/build/fonts/fa-brands-400.df02c782.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/fa-brands-400.df02c782.woff -------------------------------------------------------------------------------- /public/build/fonts/fa-regular-400.285a9d2a.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/fa-regular-400.285a9d2a.ttf -------------------------------------------------------------------------------- /public/build/fonts/fa-regular-400.5623624d.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/fa-regular-400.5623624d.woff -------------------------------------------------------------------------------- /public/build/fonts/fa-regular-400.aa66d0e0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/fa-regular-400.aa66d0e0.eot -------------------------------------------------------------------------------- /public/build/fonts/fa-regular-400.ac21cac3.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/fa-regular-400.ac21cac3.woff2 -------------------------------------------------------------------------------- /public/build/fonts/fa-solid-900.3ded831d.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/fa-solid-900.3ded831d.woff -------------------------------------------------------------------------------- /public/build/fonts/fa-solid-900.42e1fbd2.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/fa-solid-900.42e1fbd2.eot -------------------------------------------------------------------------------- /public/build/fonts/fa-solid-900.896e20e2.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/fa-solid-900.896e20e2.ttf -------------------------------------------------------------------------------- /public/build/fonts/fa-solid-900.d6d8d5da.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/fa-solid-900.d6d8d5da.woff2 -------------------------------------------------------------------------------- /public/build/fonts/lato-bold-italic.0b6bb672.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/lato-bold-italic.0b6bb672.woff2 -------------------------------------------------------------------------------- /public/build/fonts/lato-bold-italic.9c7e4e9e.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/lato-bold-italic.9c7e4e9e.woff -------------------------------------------------------------------------------- /public/build/fonts/lato-bold.cccb8974.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/lato-bold.cccb8974.woff2 -------------------------------------------------------------------------------- /public/build/fonts/lato-bold.d878b6c2.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/lato-bold.d878b6c2.woff -------------------------------------------------------------------------------- /public/build/fonts/lato-normal-italic.4eb103b4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/lato-normal-italic.4eb103b4.woff2 -------------------------------------------------------------------------------- /public/build/fonts/lato-normal-italic.f28f2d64.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/lato-normal-italic.f28f2d64.woff -------------------------------------------------------------------------------- /public/build/fonts/lato-normal.27bd77b9.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/lato-normal.27bd77b9.woff -------------------------------------------------------------------------------- /public/build/fonts/lato-normal.bd03a2cc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/public/build/fonts/lato-normal.bd03a2cc.woff2 -------------------------------------------------------------------------------- /public/build/login.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([["login"],{"23AV":function(n,a,i){(function(n){n((function(){var a=n("#username"),i=n("#password");a.val()&&"jane_admin"!==a.val()||(a.val("jane_admin"),i.val("kitten"))}))}).call(this,i("EVdn"))}},[["23AV","runtime",0]]]); -------------------------------------------------------------------------------- /public/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "build/0.js": "/build/0.js", 3 | "build/1.js": "/build/1.js", 4 | "build/2.js": "/build/2.js", 5 | "build/admin.css": "/build/admin.css", 6 | "build/admin.js": "/build/admin.js", 7 | "build/app.css": "/build/app.css", 8 | "build/app.js": "/build/app.js", 9 | "build/login.js": "/build/login.js", 10 | "build/runtime.js": "/build/runtime.js", 11 | "build/search.js": "/build/search.js", 12 | "build/fonts/fa-brands-400.eot": "/build/fonts/fa-brands-400.14c590d1.eot", 13 | "build/fonts/fa-brands-400.woff2": "/build/fonts/fa-brands-400.3e1b2a65.woff2", 14 | "build/fonts/fa-brands-400.ttf": "/build/fonts/fa-brands-400.5e8aa9ea.ttf", 15 | "build/fonts/fa-brands-400.woff": "/build/fonts/fa-brands-400.df02c782.woff", 16 | "build/fonts/fa-regular-400.ttf": "/build/fonts/fa-regular-400.285a9d2a.ttf", 17 | "build/fonts/fa-regular-400.woff": "/build/fonts/fa-regular-400.5623624d.woff", 18 | "build/fonts/fa-regular-400.eot": "/build/fonts/fa-regular-400.aa66d0e0.eot", 19 | "build/fonts/fa-regular-400.woff2": "/build/fonts/fa-regular-400.ac21cac3.woff2", 20 | "build/fonts/fa-solid-900.woff": "/build/fonts/fa-solid-900.3ded831d.woff", 21 | "build/fonts/fa-solid-900.eot": "/build/fonts/fa-solid-900.42e1fbd2.eot", 22 | "build/fonts/fa-solid-900.ttf": "/build/fonts/fa-solid-900.896e20e2.ttf", 23 | "build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.d6d8d5da.woff2", 24 | "build/fonts/glyphicons-halflings-regular.woff2": "/build/fonts/glyphicons-halflings-regular.448c34a5.woff2", 25 | "build/fonts/glyphicons-halflings-regular.ttf": "/build/fonts/glyphicons-halflings-regular.e18bbf61.ttf", 26 | "build/fonts/glyphicons-halflings-regular.eot": "/build/fonts/glyphicons-halflings-regular.f4769f9b.eot", 27 | "build/fonts/glyphicons-halflings-regular.woff": "/build/fonts/glyphicons-halflings-regular.fa277232.woff", 28 | "build/fonts/lato-bold-italic.woff2": "/build/fonts/lato-bold-italic.0b6bb672.woff2", 29 | "build/fonts/lato-bold-italic.woff": "/build/fonts/lato-bold-italic.9c7e4e9e.woff", 30 | "build/fonts/lato-bold.woff2": "/build/fonts/lato-bold.cccb8974.woff2", 31 | "build/fonts/lato-bold.woff": "/build/fonts/lato-bold.d878b6c2.woff", 32 | "build/fonts/lato-normal-italic.woff2": "/build/fonts/lato-normal-italic.4eb103b4.woff2", 33 | "build/fonts/lato-normal-italic.woff": "/build/fonts/lato-normal-italic.f28f2d64.woff", 34 | "build/fonts/lato-normal.woff": "/build/fonts/lato-normal.27bd77b9.woff", 35 | "build/fonts/lato-normal.woff2": "/build/fonts/lato-normal.bd03a2cc.woff2", 36 | "build/images/fa-brands-400.svg": "/build/images/fa-brands-400.bfa9c38b.svg", 37 | "build/images/fa-regular-400.svg": "/build/images/fa-regular-400.95f13e0b.svg", 38 | "build/images/fa-solid-900.svg": "/build/images/fa-solid-900.6ed5e3bc.svg", 39 | "build/images/glyphicons-halflings-regular.svg": "/build/images/glyphicons-halflings-regular.89889688.svg" 40 | } -------------------------------------------------------------------------------- /public/build/runtime.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,l,i=r[0],f=r[1],a=r[2],c=0,s=[];chandle($request); 26 | $response->send(); 27 | $kernel->terminate($request, $response); 28 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | # www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449 3 | 4 | User-agent: * 5 | -------------------------------------------------------------------------------- /src/Controller/DomainController.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Controller; 13 | 14 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 15 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 16 | use Symfony\Component\HttpFoundation\Response; 17 | 18 | /** 19 | * Controller used to test domains. 20 | * 21 | * @Route("/domain") 22 | */ 23 | class DomainController extends Controller 24 | { 25 | /** 26 | * @Route("/", name="domain_index", host="example.com") 27 | */ 28 | public function domainAction() 29 | { 30 | return Response::create('DOMAIN example.com'); 31 | } 32 | 33 | /** 34 | * @Route("/", name="subdomain_index", host="{subdomain}.example.com") 35 | */ 36 | public function subdomainAction($subdomain) 37 | { 38 | return Response::create('SUBDOMAIN '.$subdomain); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Controller/SecurityController.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Controller; 13 | 14 | use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; 15 | use Symfony\Component\HttpFoundation\Request; 16 | use Symfony\Component\HttpFoundation\Response; 17 | use Symfony\Component\Routing\Annotation\Route; 18 | use Symfony\Component\Security\Core\Security; 19 | use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; 20 | use Symfony\Component\Security\Http\Util\TargetPathTrait; 21 | 22 | /** 23 | * Controller used to manage the application security. 24 | * See https://symfony.com/doc/current/cookbook/security/form_login_setup.html. 25 | * 26 | * @author Ryan Weaver 27 | * @author Javier Eguiluz 28 | */ 29 | class SecurityController extends AbstractController 30 | { 31 | use TargetPathTrait; 32 | 33 | /** 34 | * @Route("/login", name="security_login") 35 | */ 36 | public function login(Request $request, Security $security, AuthenticationUtils $helper): Response 37 | { 38 | // if user is already logged in, don't display the login page again 39 | if ($security->isGranted('ROLE_USER')) { 40 | return $this->redirectToRoute('blog_index'); 41 | } 42 | 43 | // this statement solves an edge-case: if you change the locale in the login 44 | // page, after a successful login you are redirected to a page in the previous 45 | // locale. This code regenerates the referrer URL whenever the login page is 46 | // browsed, to ensure that its locale is always the current one. 47 | $this->saveTargetPath($request->getSession(), 'main', $this->generateUrl('admin_index')); 48 | 49 | return $this->render('security/login.html.twig', [ 50 | // last username entered by the user (if any) 51 | 'last_username' => $helper->getLastUsername(), 52 | // last authentication error (if any) 53 | 'error' => $helper->getLastAuthenticationError(), 54 | ]); 55 | } 56 | 57 | /** 58 | * This is the route the user can use to logout. 59 | * 60 | * But, this will never be executed. Symfony will intercept this first 61 | * and handle the logout automatically. See logout in config/packages/security.yaml 62 | * 63 | * @Route("/logout", name="security_logout") 64 | */ 65 | public function logout(): void 66 | { 67 | throw new \Exception('This should never be reached!'); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Controller/UserController.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Controller; 13 | 14 | use App\Form\Type\ChangePasswordType; 15 | use App\Form\UserType; 16 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; 17 | use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; 18 | use Symfony\Component\HttpFoundation\Request; 19 | use Symfony\Component\HttpFoundation\Response; 20 | use Symfony\Component\Routing\Annotation\Route; 21 | use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; 22 | 23 | /** 24 | * Controller used to manage current user. 25 | * 26 | * @Route("/profile") 27 | * @IsGranted("ROLE_USER") 28 | * 29 | * @author Romain Monteil 30 | */ 31 | class UserController extends AbstractController 32 | { 33 | /** 34 | * @Route("/edit", methods={"GET", "POST"}, name="user_edit") 35 | */ 36 | public function edit(Request $request): Response 37 | { 38 | $user = $this->getUser(); 39 | 40 | $form = $this->createForm(UserType::class, $user); 41 | $form->handleRequest($request); 42 | 43 | if ($form->isSubmitted() && $form->isValid()) { 44 | $this->getDoctrine()->getManager()->flush(); 45 | 46 | $this->addFlash('success', 'user.updated_successfully'); 47 | 48 | return $this->redirectToRoute('user_edit'); 49 | } 50 | 51 | return $this->render('user/edit.html.twig', [ 52 | 'user' => $user, 53 | 'form' => $form->createView(), 54 | ]); 55 | } 56 | 57 | /** 58 | * @Route("/change-password", methods={"GET", "POST"}, name="user_change_password") 59 | */ 60 | public function changePassword(Request $request, UserPasswordEncoderInterface $encoder): Response 61 | { 62 | $user = $this->getUser(); 63 | 64 | $form = $this->createForm(ChangePasswordType::class); 65 | $form->handleRequest($request); 66 | 67 | if ($form->isSubmitted() && $form->isValid()) { 68 | $user->setPassword($encoder->encodePassword($user, $form->get('newPassword')->getData())); 69 | 70 | $this->getDoctrine()->getManager()->flush(); 71 | 72 | return $this->redirectToRoute('security_logout'); 73 | } 74 | 75 | return $this->render('user/change_password.html.twig', [ 76 | 'form' => $form->createView(), 77 | ]); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Entity/Tag.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Entity; 13 | 14 | use Doctrine\ORM\Mapping as ORM; 15 | 16 | /** 17 | * @ORM\Entity() 18 | * @ORM\Table(name="symfony_demo_tag") 19 | * 20 | * Defines the properties of the Tag entity to represent the post tags. 21 | * 22 | * See https://symfony.com/doc/current/book/doctrine.html#creating-an-entity-class 23 | * 24 | * @author Yonel Ceruto 25 | */ 26 | class Tag implements \JsonSerializable 27 | { 28 | /** 29 | * @var int 30 | * 31 | * @ORM\Id 32 | * @ORM\GeneratedValue 33 | * @ORM\Column(type="integer") 34 | */ 35 | private $id; 36 | 37 | /** 38 | * @var string 39 | * 40 | * @ORM\Column(type="string", unique=true) 41 | */ 42 | private $name; 43 | 44 | public function getId(): ?int 45 | { 46 | return $this->id; 47 | } 48 | 49 | public function setName(string $name): void 50 | { 51 | $this->name = $name; 52 | } 53 | 54 | public function getName(): ?string 55 | { 56 | return $this->name; 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function jsonSerialize(): string 63 | { 64 | // This entity implements JsonSerializable (http://php.net/manual/en/class.jsonserializable.php) 65 | // so this method is used to customize its JSON representation when json_encode() 66 | // is called, for example in tags|json_encode (app/Resources/views/form/fields.html.twig) 67 | 68 | return $this->name; 69 | } 70 | 71 | public function __toString(): string 72 | { 73 | return $this->name; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/EventSubscriber/CommentNotificationSubscriber.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\EventSubscriber; 13 | 14 | use App\Entity\Comment; 15 | use App\Events\CommentCreatedEvent; 16 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 17 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 18 | use Symfony\Contracts\Translation\TranslatorInterface; 19 | 20 | /** 21 | * Notifies post's author about new comments. 22 | * 23 | * @author Oleg Voronkovich 24 | */ 25 | class CommentNotificationSubscriber implements EventSubscriberInterface 26 | { 27 | private $mailer; 28 | private $translator; 29 | private $urlGenerator; 30 | private $sender; 31 | 32 | public function __construct(\Swift_Mailer $mailer, UrlGeneratorInterface $urlGenerator, TranslatorInterface $translator, $sender) 33 | { 34 | $this->mailer = $mailer; 35 | $this->urlGenerator = $urlGenerator; 36 | $this->translator = $translator; 37 | $this->sender = $sender; 38 | } 39 | 40 | public static function getSubscribedEvents(): array 41 | { 42 | return [ 43 | CommentCreatedEvent::class => 'onCommentCreated', 44 | ]; 45 | } 46 | 47 | public function onCommentCreated(CommentCreatedEvent $event): void 48 | { 49 | /** @var Comment $comment */ 50 | $comment = $event->getComment(); 51 | $post = $comment->getPost(); 52 | 53 | $linkToPost = $this->urlGenerator->generate('blog_post', [ 54 | 'slug' => $post->getSlug(), 55 | '_fragment' => 'comment_'.$comment->getId(), 56 | ], UrlGeneratorInterface::ABSOLUTE_URL); 57 | 58 | $subject = $this->translator->trans('notification.comment_created'); 59 | $body = $this->translator->trans('notification.comment_created.description', [ 60 | '%title%' => $post->getTitle(), 61 | '%link%' => $linkToPost, 62 | ]); 63 | 64 | // Symfony uses a library called SwiftMailer to send emails. That's why 65 | // email messages are created instantiating a Swift_Message class. 66 | // See https://symfony.com/doc/current/email.html#sending-emails 67 | $message = (new \Swift_Message()) 68 | ->setSubject($subject) 69 | ->setTo($post->getAuthor()->getEmail()) 70 | ->setFrom($this->sender) 71 | ->setBody($body, 'text/html') 72 | ; 73 | 74 | // In config/packages/dev/swiftmailer.yaml the 'disable_delivery' option is set to 'true'. 75 | // That's why in the development environment you won't actually receive any email. 76 | // However, you can inspect the contents of those unsent emails using the debug toolbar. 77 | // See https://symfony.com/doc/current/email/dev_environment.html#viewing-from-the-web-debug-toolbar 78 | $this->mailer->send($message); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/EventSubscriber/ControllerSubscriber.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\EventSubscriber; 13 | 14 | use App\Twig\SourceCodeExtension; 15 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 16 | use Symfony\Component\HttpKernel\Event\ControllerEvent; 17 | use Symfony\Component\HttpKernel\KernelEvents; 18 | 19 | /** 20 | * Defines the method that 'listens' to the 'kernel.controller' event, which is 21 | * triggered whenever a controller is executed in the application. 22 | * 23 | * @author Ryan Weaver 24 | * @author Javier Eguiluz 25 | */ 26 | class ControllerSubscriber implements EventSubscriberInterface 27 | { 28 | private $twigExtension; 29 | 30 | public function __construct(SourceCodeExtension $twigExtension) 31 | { 32 | $this->twigExtension = $twigExtension; 33 | } 34 | 35 | public static function getSubscribedEvents(): array 36 | { 37 | return [ 38 | KernelEvents::CONTROLLER => 'registerCurrentController', 39 | ]; 40 | } 41 | 42 | public function registerCurrentController(ControllerEvent $event): void 43 | { 44 | // this check is needed because in Symfony a request can perform any 45 | // number of sub-requests. See 46 | // https://symfony.com/doc/current/components/http_kernel/introduction.html#sub-requests 47 | if ($event->isMasterRequest()) { 48 | $this->twigExtension->setController($event->getController()); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/EventSubscriber/RedirectToPreferredLocaleSubscriber.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\EventSubscriber; 13 | 14 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 15 | use Symfony\Component\HttpFoundation\RedirectResponse; 16 | use Symfony\Component\HttpKernel\Event\RequestEvent; 17 | use Symfony\Component\HttpKernel\KernelEvents; 18 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 19 | 20 | /** 21 | * When visiting the homepage, this listener redirects the user to the most 22 | * appropriate localized version according to the browser settings. 23 | * 24 | * See https://symfony.com/doc/current/components/http_kernel/introduction.html#the-kernel-request-event 25 | * 26 | * @author Oleg Voronkovich 27 | */ 28 | class RedirectToPreferredLocaleSubscriber implements EventSubscriberInterface 29 | { 30 | private $urlGenerator; 31 | private $locales; 32 | private $defaultLocale; 33 | 34 | public function __construct(UrlGeneratorInterface $urlGenerator, string $locales, string $defaultLocale = null) 35 | { 36 | $this->urlGenerator = $urlGenerator; 37 | 38 | $this->locales = explode('|', trim($locales)); 39 | if (empty($this->locales)) { 40 | throw new \UnexpectedValueException('The list of supported locales must not be empty.'); 41 | } 42 | 43 | $this->defaultLocale = $defaultLocale ?: $this->locales[0]; 44 | 45 | if (!\in_array($this->defaultLocale, $this->locales, true)) { 46 | throw new \UnexpectedValueException(sprintf('The default locale ("%s") must be one of "%s".', $this->defaultLocale, $locales)); 47 | } 48 | 49 | // Add the default locale at the first position of the array, 50 | // because Symfony\HttpFoundation\Request::getPreferredLanguage 51 | // returns the first element when no an appropriate language is found 52 | array_unshift($this->locales, $this->defaultLocale); 53 | $this->locales = array_unique($this->locales); 54 | } 55 | 56 | public static function getSubscribedEvents(): array 57 | { 58 | return [ 59 | KernelEvents::REQUEST => 'onKernelRequest', 60 | ]; 61 | } 62 | 63 | public function onKernelRequest(RequestEvent $event): void 64 | { 65 | $request = $event->getRequest(); 66 | 67 | // Ignore sub-requests and all URLs but the homepage 68 | if (!$event->isMasterRequest() || '/' !== $request->getPathInfo()) { 69 | return; 70 | } 71 | // Ignore requests from referrers with the same HTTP host in order to prevent 72 | // changing language for users who possibly already selected it for this application. 73 | if (0 === mb_stripos($request->headers->get('referer'), $request->getSchemeAndHttpHost())) { 74 | return; 75 | } 76 | 77 | $preferredLanguage = $request->getPreferredLanguage($this->locales); 78 | 79 | if ($preferredLanguage !== $this->defaultLocale) { 80 | $response = new RedirectResponse($this->urlGenerator->generate('homepage', ['_locale' => $preferredLanguage])); 81 | $event->setResponse($response); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Events/CommentCreatedEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Events; 13 | 14 | use App\Entity\Comment; 15 | use Symfony\Contracts\EventDispatcher\Event; 16 | 17 | class CommentCreatedEvent extends Event 18 | { 19 | protected $comment; 20 | 21 | public function __construct(Comment $comment) 22 | { 23 | $this->comment = $comment; 24 | } 25 | 26 | public function getComment(): Comment 27 | { 28 | return $this->comment; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Form/CommentType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Form; 13 | 14 | use App\Entity\Comment; 15 | use Symfony\Component\Form\AbstractType; 16 | use Symfony\Component\Form\Extension\Core\Type\TextareaType; 17 | use Symfony\Component\Form\FormBuilderInterface; 18 | use Symfony\Component\OptionsResolver\OptionsResolver; 19 | 20 | /** 21 | * Defines the form used to create and manipulate blog comments. Although in this 22 | * case the form is trivial and we could build it inside the controller, a good 23 | * practice is to always define your forms as classes. 24 | * 25 | * See https://symfony.com/doc/current/book/forms.html#creating-form-classes 26 | * 27 | * @author Ryan Weaver 28 | * @author Javier Eguiluz 29 | */ 30 | class CommentType extends AbstractType 31 | { 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function buildForm(FormBuilderInterface $builder, array $options): void 36 | { 37 | // By default, form fields include the 'required' attribute, which enables 38 | // the client-side form validation. This means that you can't test the 39 | // server-side validation errors from the browser. To temporarily disable 40 | // this validation, set the 'required' attribute to 'false': 41 | // $builder->add('content', null, ['required' => false]); 42 | 43 | $builder 44 | ->add('content', TextareaType::class, [ 45 | 'help' => 'help.comment_content', 46 | ]) 47 | ; 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function configureOptions(OptionsResolver $resolver): void 54 | { 55 | $resolver->setDefaults([ 56 | 'data_class' => Comment::class, 57 | ]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Form/DataTransformer/TagArrayToStringTransformer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Form\DataTransformer; 13 | 14 | use App\Entity\Tag; 15 | use App\Repository\TagRepository; 16 | use Symfony\Component\Form\DataTransformerInterface; 17 | 18 | /** 19 | * This data transformer is used to translate the array of tags into a comma separated format 20 | * that can be displayed and managed by Bootstrap-tagsinput js plugin (and back on submit). 21 | * 22 | * See https://symfony.com/doc/current/form/data_transformers.html 23 | * 24 | * @author Yonel Ceruto 25 | * @author Jonathan Boyer 26 | */ 27 | class TagArrayToStringTransformer implements DataTransformerInterface 28 | { 29 | private $tags; 30 | 31 | public function __construct(TagRepository $tags) 32 | { 33 | $this->tags = $tags; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function transform($tags): string 40 | { 41 | // The value received is an array of Tag objects generated with 42 | // Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::transform() 43 | // The value returned is a string that concatenates the string representation of those objects 44 | 45 | /* @var Tag[] $tags */ 46 | return implode(',', $tags); 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function reverseTransform($string): array 53 | { 54 | if ('' === $string || null === $string) { 55 | return []; 56 | } 57 | 58 | $names = array_filter(array_unique(array_map('trim', explode(',', $string)))); 59 | 60 | // Get the current tags and find the new ones that should be created. 61 | $tags = $this->tags->findBy([ 62 | 'name' => $names, 63 | ]); 64 | $newNames = array_diff($names, $tags); 65 | foreach ($newNames as $name) { 66 | $tag = new Tag(); 67 | $tag->setName($name); 68 | $tags[] = $tag; 69 | 70 | // There's no need to persist these new tags because Doctrine does that automatically 71 | // thanks to the cascade={"persist"} option in the App\Entity\Post::$tags property. 72 | } 73 | 74 | // Return an array of tags to transform them back into a Doctrine Collection. 75 | // See Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::reverseTransform() 76 | return $tags; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Form/PostType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Form; 13 | 14 | use App\Entity\Post; 15 | use App\Form\Type\DateTimePickerType; 16 | use App\Form\Type\TagsInputType; 17 | use Symfony\Component\Form\AbstractType; 18 | use Symfony\Component\Form\Extension\Core\Type\TextareaType; 19 | use Symfony\Component\Form\FormBuilderInterface; 20 | use Symfony\Component\OptionsResolver\OptionsResolver; 21 | 22 | /** 23 | * Defines the form used to create and manipulate blog posts. 24 | * 25 | * @author Ryan Weaver 26 | * @author Javier Eguiluz 27 | * @author Yonel Ceruto 28 | */ 29 | class PostType extends AbstractType 30 | { 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function buildForm(FormBuilderInterface $builder, array $options): void 35 | { 36 | // For the full reference of options defined by each form field type 37 | // see https://symfony.com/doc/current/reference/forms/types.html 38 | 39 | // By default, form fields include the 'required' attribute, which enables 40 | // the client-side form validation. This means that you can't test the 41 | // server-side validation errors from the browser. To temporarily disable 42 | // this validation, set the 'required' attribute to 'false': 43 | // $builder->add('title', null, ['required' => false, ...]); 44 | 45 | $builder 46 | ->add('title', null, [ 47 | 'attr' => ['autofocus' => true], 48 | 'label' => 'label.title', 49 | ]) 50 | ->add('summary', TextareaType::class, [ 51 | 'help' => 'help.post_summary', 52 | 'label' => 'label.summary', 53 | ]) 54 | ->add('content', null, [ 55 | 'attr' => ['rows' => 20], 56 | 'help' => 'help.post_content', 57 | 'label' => 'label.content', 58 | ]) 59 | ->add('publishedAt', DateTimePickerType::class, [ 60 | 'label' => 'label.published_at', 61 | 'help' => 'help.post_publication', 62 | ]) 63 | ->add('tags', TagsInputType::class, [ 64 | 'label' => 'label.tags', 65 | 'required' => false, 66 | ]) 67 | ; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function configureOptions(OptionsResolver $resolver): void 74 | { 75 | $resolver->setDefaults([ 76 | 'data_class' => Post::class, 77 | ]); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Form/Type/ChangePasswordType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Form\Type; 13 | 14 | use Symfony\Component\Form\AbstractType; 15 | use Symfony\Component\Form\Extension\Core\Type\PasswordType; 16 | use Symfony\Component\Form\Extension\Core\Type\RepeatedType; 17 | use Symfony\Component\Form\FormBuilderInterface; 18 | use Symfony\Component\Security\Core\Validator\Constraints\UserPassword; 19 | use Symfony\Component\Validator\Constraints\Length; 20 | use Symfony\Component\Validator\Constraints\NotBlank; 21 | 22 | /** 23 | * Defines the custom form field type used to change user's password. 24 | * 25 | * @author Romain Monteil 26 | */ 27 | class ChangePasswordType extends AbstractType 28 | { 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function buildForm(FormBuilderInterface $builder, array $options): void 33 | { 34 | $builder 35 | ->add('currentPassword', PasswordType::class, [ 36 | 'constraints' => [ 37 | new UserPassword(), 38 | ], 39 | 'label' => 'label.current_password', 40 | 'attr' => [ 41 | 'autocomplete' => 'off', 42 | ], 43 | ]) 44 | ->add('newPassword', RepeatedType::class, [ 45 | 'type' => PasswordType::class, 46 | 'constraints' => [ 47 | new NotBlank(), 48 | new Length([ 49 | 'min' => 5, 50 | 'max' => 128, 51 | ]), 52 | ], 53 | 'first_options' => [ 54 | 'label' => 'label.new_password', 55 | ], 56 | 'second_options' => [ 57 | 'label' => 'label.new_password_confirm', 58 | ], 59 | ]) 60 | ; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Form/Type/DateTimePickerType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Form\Type; 13 | 14 | use App\Utils\MomentFormatConverter; 15 | use Symfony\Component\Form\AbstractType; 16 | use Symfony\Component\Form\Extension\Core\Type\DateTimeType; 17 | use Symfony\Component\Form\FormInterface; 18 | use Symfony\Component\Form\FormView; 19 | use Symfony\Component\OptionsResolver\OptionsResolver; 20 | 21 | /** 22 | * Defines the custom form field type used to manipulate datetime values across 23 | * Bootstrap Date\Time Picker javascript plugin. 24 | * 25 | * See https://symfony.com/doc/current/cookbook/form/create_custom_field_type.html 26 | * 27 | * @author Yonel Ceruto 28 | */ 29 | class DateTimePickerType extends AbstractType 30 | { 31 | private $formatConverter; 32 | 33 | public function __construct(MomentFormatConverter $converter) 34 | { 35 | $this->formatConverter = $converter; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function buildView(FormView $view, FormInterface $form, array $options): void 42 | { 43 | $view->vars['attr']['data-date-format'] = $this->formatConverter->convert($options['format']); 44 | $view->vars['attr']['data-date-locale'] = mb_strtolower(str_replace('_', '-', \Locale::getDefault())); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function configureOptions(OptionsResolver $resolver): void 51 | { 52 | $resolver->setDefaults([ 53 | 'widget' => 'single_text', 54 | // if true, the browser will display the native date picker widget 55 | // however, this app uses a custom JavaScript widget, so it must be set to false 56 | 'html5' => false, 57 | ]); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function getParent() 64 | { 65 | return DateTimeType::class; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Form/Type/TagsInputType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Form\Type; 13 | 14 | use App\Form\DataTransformer\TagArrayToStringTransformer; 15 | use App\Repository\TagRepository; 16 | use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; 17 | use Symfony\Component\Form\AbstractType; 18 | use Symfony\Component\Form\Extension\Core\Type\TextType; 19 | use Symfony\Component\Form\FormBuilderInterface; 20 | use Symfony\Component\Form\FormInterface; 21 | use Symfony\Component\Form\FormView; 22 | 23 | /** 24 | * Defines the custom form field type used to manipulate tags values across 25 | * Bootstrap-tagsinput javascript plugin. 26 | * 27 | * See https://symfony.com/doc/current/cookbook/form/create_custom_field_type.html 28 | * 29 | * @author Yonel Ceruto 30 | */ 31 | class TagsInputType extends AbstractType 32 | { 33 | private $tags; 34 | 35 | public function __construct(TagRepository $tags) 36 | { 37 | $this->tags = $tags; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function buildForm(FormBuilderInterface $builder, array $options): void 44 | { 45 | $builder 46 | // The Tag collection must be transformed into a comma separated string. 47 | // We could create a custom transformer to do Collection <-> string in one step, 48 | // but here we're doing the transformation in two steps (Collection <-> array <-> string) 49 | // and reuse the existing CollectionToArrayTransformer. 50 | ->addModelTransformer(new CollectionToArrayTransformer(), true) 51 | ->addModelTransformer(new TagArrayToStringTransformer($this->tags), true) 52 | ; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function buildView(FormView $view, FormInterface $form, array $options): void 59 | { 60 | $view->vars['tags'] = $this->tags->findAll(); 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function getParent() 67 | { 68 | return TextType::class; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Form/UserType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Form; 13 | 14 | use App\Entity\User; 15 | use Symfony\Component\Form\AbstractType; 16 | use Symfony\Component\Form\Extension\Core\Type\EmailType; 17 | use Symfony\Component\Form\Extension\Core\Type\TextType; 18 | use Symfony\Component\Form\FormBuilderInterface; 19 | use Symfony\Component\OptionsResolver\OptionsResolver; 20 | 21 | /** 22 | * Defines the form used to edit an user. 23 | * 24 | * @author Romain Monteil 25 | */ 26 | class UserType extends AbstractType 27 | { 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function buildForm(FormBuilderInterface $builder, array $options): void 32 | { 33 | // For the full reference of options defined by each form field type 34 | // see https://symfony.com/doc/current/reference/forms/types.html 35 | 36 | // By default, form fields include the 'required' attribute, which enables 37 | // the client-side form validation. This means that you can't test the 38 | // server-side validation errors from the browser. To temporarily disable 39 | // this validation, set the 'required' attribute to 'false': 40 | // $builder->add('title', null, ['required' => false, ...]); 41 | 42 | $builder 43 | ->add('username', TextType::class, [ 44 | 'label' => 'label.username', 45 | 'disabled' => true, 46 | ]) 47 | ->add('fullName', TextType::class, [ 48 | 'label' => 'label.fullname', 49 | ]) 50 | ->add('email', EmailType::class, [ 51 | 'label' => 'label.email', 52 | ]) 53 | ; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function configureOptions(OptionsResolver $resolver): void 60 | { 61 | $resolver->setDefaults([ 62 | 'data_class' => User::class, 63 | ]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App; 13 | 14 | use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; 15 | use Symfony\Component\Config\Loader\LoaderInterface; 16 | use Symfony\Component\Config\Resource\FileResource; 17 | use Symfony\Component\DependencyInjection\ContainerBuilder; 18 | use Symfony\Component\HttpKernel\Kernel as BaseKernel; 19 | use Symfony\Component\Routing\RouteCollectionBuilder; 20 | 21 | class Kernel extends BaseKernel 22 | { 23 | use MicroKernelTrait; 24 | 25 | const CONFIG_EXTS = '.{php,xml,yaml,yml}'; 26 | 27 | public function getCacheDir() 28 | { 29 | return $this->getProjectDir().'/var/cache/'.$this->environment; 30 | } 31 | 32 | public function getLogDir() 33 | { 34 | return $this->getProjectDir().'/var/log'; 35 | } 36 | 37 | public function registerBundles() 38 | { 39 | $contents = require $this->getProjectDir().'/config/bundles.php'; 40 | foreach ($contents as $class => $envs) { 41 | if ($envs[$this->environment] ?? $envs['all'] ?? false) { 42 | yield new $class(); 43 | } 44 | } 45 | } 46 | 47 | protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader) 48 | { 49 | $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php')); 50 | $container->setParameter('container.dumper.inline_class_loader', true); 51 | $confDir = $this->getProjectDir().'/config'; 52 | 53 | $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); 54 | $loader->load($confDir.'/{packages}/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob'); 55 | $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob'); 56 | $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob'); 57 | } 58 | 59 | protected function configureRoutes(RouteCollectionBuilder $routes) 60 | { 61 | $confDir = $this->getProjectDir().'/config'; 62 | 63 | $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob'); 64 | $routes->import($confDir.'/{routes}/'.$this->environment.'/**/*'.self::CONFIG_EXTS, '/', 'glob'); 65 | $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Migrations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/src/Migrations/.gitignore -------------------------------------------------------------------------------- /src/Pagination/Paginator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Pagination; 13 | 14 | use Doctrine\ORM\QueryBuilder as DoctrineQueryBuilder; 15 | use Doctrine\ORM\Tools\Pagination\CountWalker; 16 | use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator; 17 | 18 | /** 19 | * @author Javier Eguiluz 20 | */ 21 | class Paginator 22 | { 23 | private const PAGE_SIZE = 10; 24 | private $queryBuilder; 25 | private $currentPage; 26 | private $pageSize; 27 | private $results; 28 | private $numResults; 29 | 30 | public function __construct(DoctrineQueryBuilder $queryBuilder, int $pageSize = self::PAGE_SIZE) 31 | { 32 | $this->queryBuilder = $queryBuilder; 33 | $this->pageSize = $pageSize; 34 | } 35 | 36 | public function paginate(int $page = 1): self 37 | { 38 | $this->currentPage = max(1, $page); 39 | $firstResult = ($this->currentPage - 1) * $this->pageSize; 40 | 41 | $query = $this->queryBuilder 42 | ->setFirstResult($firstResult) 43 | ->setMaxResults($this->pageSize) 44 | ->getQuery(); 45 | 46 | if (0 === \count($this->queryBuilder->getDQLPart('join'))) { 47 | $query->setHint(CountWalker::HINT_DISTINCT, false); 48 | } 49 | 50 | $paginator = new DoctrinePaginator($query, true); 51 | 52 | $useOutputWalkers = \count($this->queryBuilder->getDQLPart('having') ?: []) > 0; 53 | $paginator->setUseOutputWalkers($useOutputWalkers); 54 | 55 | $this->results = $paginator->getIterator(); 56 | $this->numResults = $paginator->count(); 57 | 58 | return $this; 59 | } 60 | 61 | public function getCurrentPage(): int 62 | { 63 | return $this->currentPage; 64 | } 65 | 66 | public function getLastPage(): int 67 | { 68 | return (int) ceil($this->numResults / $this->pageSize); 69 | } 70 | 71 | public function getPageSize(): int 72 | { 73 | return $this->pageSize; 74 | } 75 | 76 | public function hasPreviousPage(): bool 77 | { 78 | return $this->currentPage > 1; 79 | } 80 | 81 | public function getPreviousPage(): int 82 | { 83 | return max(1, $this->currentPage - 1); 84 | } 85 | 86 | public function hasNextPage(): bool 87 | { 88 | return $this->currentPage < $this->getLastPage(); 89 | } 90 | 91 | public function getNextPage(): int 92 | { 93 | return min($this->getLastPage(), $this->currentPage + 1); 94 | } 95 | 96 | public function hasToPaginate(): bool 97 | { 98 | return $this->numResults > $this->pageSize; 99 | } 100 | 101 | public function getNumResults(): int 102 | { 103 | return $this->numResults; 104 | } 105 | 106 | public function getResults(): \Traversable 107 | { 108 | return $this->results; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Repository/PostRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Repository; 13 | 14 | use App\Entity\Post; 15 | use App\Entity\Tag; 16 | use App\Pagination\Paginator; 17 | use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; 18 | use Doctrine\Common\Persistence\ManagerRegistry; 19 | 20 | /** 21 | * This custom Doctrine repository contains some methods which are useful when 22 | * querying for blog post information. 23 | * 24 | * See https://symfony.com/doc/current/doctrine/repository.html 25 | * 26 | * @author Ryan Weaver 27 | * @author Javier Eguiluz 28 | * @author Yonel Ceruto 29 | */ 30 | class PostRepository extends ServiceEntityRepository 31 | { 32 | public function __construct(ManagerRegistry $registry) 33 | { 34 | parent::__construct($registry, Post::class); 35 | } 36 | 37 | public function findLatest(int $page = 1, Tag $tag = null): Paginator 38 | { 39 | $qb = $this->createQueryBuilder('p') 40 | ->addSelect('a', 't') 41 | ->innerJoin('p.author', 'a') 42 | ->leftJoin('p.tags', 't') 43 | ->where('p.publishedAt <= :now') 44 | ->orderBy('p.publishedAt', 'DESC') 45 | ->setParameter('now', new \DateTime()) 46 | ; 47 | 48 | if (null !== $tag) { 49 | $qb->andWhere(':tag MEMBER OF p.tags') 50 | ->setParameter('tag', $tag); 51 | } 52 | 53 | return (new Paginator($qb))->paginate($page); 54 | } 55 | 56 | /** 57 | * @return Post[] 58 | */ 59 | public function findBySearchQuery(string $query, int $limit = Post::NUM_ITEMS): array 60 | { 61 | $searchTerms = $this->extractSearchTerms($query); 62 | 63 | if (0 === \count($searchTerms)) { 64 | return []; 65 | } 66 | 67 | $queryBuilder = $this->createQueryBuilder('p'); 68 | 69 | foreach ($searchTerms as $key => $term) { 70 | $queryBuilder 71 | ->orWhere('p.title LIKE :t_'.$key) 72 | ->setParameter('t_'.$key, '%'.$term.'%') 73 | ; 74 | } 75 | 76 | return $queryBuilder 77 | ->orderBy('p.publishedAt', 'DESC') 78 | ->setMaxResults($limit) 79 | ->getQuery() 80 | ->getResult(); 81 | } 82 | 83 | /** 84 | * Transforms the search string into an array of search terms. 85 | */ 86 | private function extractSearchTerms(string $searchQuery): array 87 | { 88 | $searchQuery = trim(preg_replace('/[[:space:]]+/', ' ', $searchQuery)); 89 | $terms = array_unique(explode(' ', $searchQuery)); 90 | 91 | // ignore the search terms that are too short 92 | return array_filter($terms, function ($term) { 93 | return 2 <= mb_strlen($term); 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Repository/TagRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Repository; 13 | 14 | use App\Entity\Tag; 15 | use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; 16 | use Doctrine\Common\Persistence\ManagerRegistry; 17 | 18 | /** 19 | * This custom Doctrine repository is empty because so far we don't need any custom 20 | * method to query for application user information. But it's always a good practice 21 | * to define a custom repository that will be used when the application grows. 22 | * 23 | * See https://symfony.com/doc/current/doctrine/repository.html 24 | * 25 | * @author Yonel Ceruto 26 | */ 27 | class TagRepository extends ServiceEntityRepository 28 | { 29 | public function __construct(ManagerRegistry $registry) 30 | { 31 | parent::__construct($registry, Tag::class); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Repository/UserRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Repository; 13 | 14 | use App\Entity\User; 15 | use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; 16 | use Doctrine\Common\Persistence\ManagerRegistry; 17 | 18 | /** 19 | * This custom Doctrine repository is empty because so far we don't need any custom 20 | * method to query for application user information. But it's always a good practice 21 | * to define a custom repository that will be used when the application grows. 22 | * 23 | * See https://symfony.com/doc/current/doctrine/repository.html 24 | * 25 | * @author Ryan Weaver 26 | * @author Javier Eguiluz 27 | */ 28 | class UserRepository extends ServiceEntityRepository 29 | { 30 | public function __construct(ManagerRegistry $registry) 31 | { 32 | parent::__construct($registry, User::class); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Security/PostVoter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Security; 13 | 14 | use App\Entity\Post; 15 | use App\Entity\User; 16 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 17 | use Symfony\Component\Security\Core\Authorization\Voter\Voter; 18 | 19 | /** 20 | * It grants or denies permissions for actions related to blog posts (such as 21 | * showing, editing and deleting posts). 22 | * 23 | * See https://symfony.com/doc/current/security/voters.html 24 | * 25 | * @author Yonel Ceruto 26 | */ 27 | class PostVoter extends Voter 28 | { 29 | // Defining these constants is overkill for this simple application, but for real 30 | // applications, it's a recommended practice to avoid relying on "magic strings" 31 | public const DELETE = 'delete'; 32 | public const EDIT = 'edit'; 33 | public const SHOW = 'show'; 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | protected function supports($attribute, $subject): bool 39 | { 40 | // this voter is only executed for three specific permissions on Post objects 41 | return $subject instanceof Post && \in_array($attribute, [self::SHOW, self::EDIT, self::DELETE], true); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | protected function voteOnAttribute($attribute, $post, TokenInterface $token): bool 48 | { 49 | $user = $token->getUser(); 50 | 51 | // the user must be logged in; if not, deny permission 52 | if (!$user instanceof User) { 53 | return false; 54 | } 55 | 56 | // the logic of this voter is pretty simple: if the logged user is the 57 | // author of the given blog post, grant permission; otherwise, deny it. 58 | // (the supports() method guarantees that $post is a Post object) 59 | return $user === $post->getAuthor(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Twig/AppExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Twig; 13 | 14 | use App\Utils\Markdown; 15 | use Symfony\Component\Intl\Locales; 16 | use Twig\Extension\AbstractExtension; 17 | use Twig\TwigFilter; 18 | use Twig\TwigFunction; 19 | 20 | /** 21 | * This Twig extension adds a new 'md2html' filter to easily transform Markdown 22 | * contents into HTML contents inside Twig templates. 23 | * 24 | * See https://symfony.com/doc/current/templating/twig_extension.html 25 | * 26 | * @author Ryan Weaver 27 | * @author Javier Eguiluz 28 | * @author Julien ITARD 29 | */ 30 | class AppExtension extends AbstractExtension 31 | { 32 | private $parser; 33 | private $localeCodes; 34 | private $locales; 35 | 36 | public function __construct(Markdown $parser, string $locales) 37 | { 38 | $this->parser = $parser; 39 | 40 | $localeCodes = explode('|', $locales); 41 | sort($localeCodes); 42 | $this->localeCodes = $localeCodes; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function getFilters(): array 49 | { 50 | return [ 51 | new TwigFilter('md2html', [$this, 'markdownToHtml'], ['is_safe' => ['html']]), 52 | ]; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function getFunctions(): array 59 | { 60 | return [ 61 | new TwigFunction('locales', [$this, 'getLocales']), 62 | ]; 63 | } 64 | 65 | /** 66 | * Transforms the given Markdown content into HTML content. 67 | */ 68 | public function markdownToHtml(string $content): string 69 | { 70 | return $this->parser->toHtml($content); 71 | } 72 | 73 | /** 74 | * Takes the list of codes of the locales (languages) enabled in the 75 | * application and returns an array with the name of each locale written 76 | * in its own language (e.g. English, Français, Español, etc.). 77 | */ 78 | public function getLocales(): array 79 | { 80 | if (null !== $this->locales) { 81 | return $this->locales; 82 | } 83 | 84 | $this->locales = []; 85 | foreach ($this->localeCodes as $localeCode) { 86 | $this->locales[] = ['code' => $localeCode, 'name' => Locales::getName($localeCode, $localeCode)]; 87 | } 88 | 89 | return $this->locales; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Utils/Markdown.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Utils; 13 | 14 | use HtmlSanitizer\SanitizerInterface; 15 | 16 | /** 17 | * This class is a light interface between an external Markdown parser library 18 | * and the application. It's generally recommended to create these light interfaces 19 | * to decouple your application from the implementation details of the third-party library. 20 | * 21 | * @author Ryan Weaver 22 | * @author Javier Eguiluz 23 | */ 24 | class Markdown 25 | { 26 | private $parser; 27 | private $sanitizer; 28 | 29 | public function __construct(SanitizerInterface $sanitizer) 30 | { 31 | $this->parser = new \Parsedown(); 32 | $this->sanitizer = $sanitizer; 33 | } 34 | 35 | public function toHtml(string $text): string 36 | { 37 | $html = $this->parser->text($text); 38 | $safeHtml = $this->sanitizer->sanitize($html); 39 | 40 | return $safeHtml; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Utils/MomentFormatConverter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Utils; 13 | 14 | /** 15 | * This class is used to convert PHP date format to moment.js format. 16 | * 17 | * @author Yonel Ceruto 18 | */ 19 | class MomentFormatConverter 20 | { 21 | /** 22 | * This defines the mapping between PHP ICU date format (key) and moment.js date format (value) 23 | * For ICU formats see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax 24 | * For Moment formats see http://momentjs.com/docs/#/displaying/format/. 25 | * 26 | * @var array 27 | */ 28 | private static $formatConvertRules = [ 29 | // year 30 | 'yyyy' => 'YYYY', 'yy' => 'YY', 'y' => 'YYYY', 31 | // day 32 | 'dd' => 'DD', 'd' => 'D', 33 | // day of week 34 | 'EE' => 'ddd', 'EEEEEE' => 'dd', 35 | // timezone 36 | 'ZZZZZ' => 'Z', 'ZZZ' => 'ZZ', 37 | // letter 'T' 38 | '\'T\'' => 'T', 39 | ]; 40 | 41 | /** 42 | * Returns associated moment.js format. 43 | */ 44 | public function convert(string $format): string 45 | { 46 | return strtr($format, self::$formatConvertRules); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Utils/Slugger.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Utils; 13 | 14 | /** 15 | * @author Ryan Weaver 16 | * @author Javier Eguiluz 17 | */ 18 | class Slugger 19 | { 20 | public static function slugify(string $string): string 21 | { 22 | return preg_replace('/\s+/', '-', mb_strtolower(trim(strip_tags($string)), 'UTF-8')); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Utils/Validator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Utils; 13 | 14 | use Symfony\Component\Console\Exception\InvalidArgumentException; 15 | 16 | /** 17 | * This class is used to provide an example of integrating simple classes as 18 | * services into a Symfony application. 19 | * 20 | * @author Javier Eguiluz 21 | */ 22 | class Validator 23 | { 24 | public function validateUsername(?string $username): string 25 | { 26 | if (empty($username)) { 27 | throw new InvalidArgumentException('The username can not be empty.'); 28 | } 29 | 30 | if (1 !== preg_match('/^[a-z_]+$/', $username)) { 31 | throw new InvalidArgumentException('The username must contain only lowercase latin characters and underscores.'); 32 | } 33 | 34 | return $username; 35 | } 36 | 37 | public function validatePassword(?string $plainPassword): string 38 | { 39 | if (empty($plainPassword)) { 40 | throw new InvalidArgumentException('The password can not be empty.'); 41 | } 42 | 43 | if (mb_strlen(trim($plainPassword)) < 6) { 44 | throw new InvalidArgumentException('The password must be at least 6 characters long.'); 45 | } 46 | 47 | return $plainPassword; 48 | } 49 | 50 | public function validateEmail(?string $email): string 51 | { 52 | if (empty($email)) { 53 | throw new InvalidArgumentException('The email can not be empty.'); 54 | } 55 | 56 | if (false === mb_strpos($email, '@')) { 57 | throw new InvalidArgumentException('The email should look like a real email.'); 58 | } 59 | 60 | return $email; 61 | } 62 | 63 | public function validateFullName(?string $fullName): string 64 | { 65 | if (empty($fullName)) { 66 | throw new InvalidArgumentException('The full name can not be empty.'); 67 | } 68 | 69 | return $fullName; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /templates/admin/blog/_delete_form.html.twig: -------------------------------------------------------------------------------- 1 | {{ include('blog/_delete_post_confirmation.html.twig') }} 2 |
3 | 4 | 8 |
9 | -------------------------------------------------------------------------------- /templates/admin/blog/_form.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | By default, forms enable client-side validation. This means that you can't 3 | test the server-side validation errors from the browser. To temporarily 4 | disable this validation, add the 'novalidate' attribute: 5 | 6 | {{ form_start(form, {attr: {novalidate: 'novalidate'}}) }} 7 | #} 8 | 9 | {% if show_confirmation|default(false) %} 10 | {% set attr = {'data-confirmation': 'true'} %} 11 | {{ include('blog/_delete_post_confirmation.html.twig') }} 12 | {% endif %} 13 | 14 | {{ form_start(form, {attr: attr|default({})}) }} 15 | {{ form_widget(form) }} 16 | 17 | 20 | 21 | {% if include_back_to_home_link|default(false) %} 22 | 23 | {{ 'action.back_to_list'|trans }} 24 | 25 | {% endif %} 26 | {{ form_end(form) }} 27 | -------------------------------------------------------------------------------- /templates/admin/blog/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'admin/layout.html.twig' %} 2 | 3 | {% block body_id 'admin_post_edit' %} 4 | 5 | {% block main %} 6 |

{{ 'title.edit_post'|trans({'%id%': post.id}) }}

7 | 8 | {{ include('admin/blog/_form.html.twig', { 9 | form: form, 10 | button_label: 'action.save'|trans, 11 | include_back_to_home_link: true, 12 | }, with_context = false) }} 13 | {% endblock %} 14 | 15 | {% block sidebar %} 16 | 21 | 22 |
23 | {{ include('admin/blog/_delete_form.html.twig', {post: post}, with_context = false) }} 24 |
25 | 26 | {{ parent() }} 27 | 28 | {{ show_source_code(_self) }} 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /templates/admin/blog/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'admin/layout.html.twig' %} 2 | 3 | {% block body_id 'admin_post_index' %} 4 | 5 | {% block main %} 6 |

{{ 'title.post_list'|trans }}

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% for post in posts %} 18 | 19 | 20 | {# it's not mandatory to set the timezone in localizeddate(). This is done to 21 | avoid errors when the 'intl' PHP extension is not available and the application 22 | is forced to use the limited "intl polyfill", which only supports UTC and GMT #} 23 | 24 | 35 | 36 | {% else %} 37 | 38 | 39 | 40 | {% endfor %} 41 | 42 |
{{ 'label.title'|trans }} {{ 'label.published_at'|trans }} {{ 'label.actions'|trans }}
{{ post.title }}{{ post.publishedAt|localizeddate('medium', 'short', null, 'UTC') }} 25 | 34 |
{{ 'post.no_posts_found'|trans }}
43 | {% endblock %} 44 | 45 | {% block sidebar %} 46 | 51 | 52 | {{ parent() }} 53 | 54 | {{ show_source_code(_self) }} 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /templates/admin/blog/new.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'admin/layout.html.twig' %} 2 | 3 | {% block body_id 'admin_post_new' %} 4 | 5 | {% block main %} 6 |

{{ 'title.post_new'|trans }}

7 | 8 | {{ form_start(form) }} 9 | {{ form_row(form.title) }} 10 | {{ form_row(form.summary) }} 11 | {{ form_row(form.content) }} 12 | {{ form_row(form.publishedAt) }} 13 | {{ form_row(form.tags) }} 14 | 15 | 18 | {{ form_widget(form.saveAndCreateNew, {label: 'label.save_and_create_new', attr: {class: 'btn btn-primary'}}) }} 19 | 20 | {{ 'action.back_to_list'|trans }} 21 | 22 | {{ form_end(form) }} 23 | {% endblock %} 24 | 25 | {% block sidebar %} 26 | {{ parent() }} 27 | 28 | {{ show_source_code(_self) }} 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /templates/admin/blog/show.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'admin/layout.html.twig' %} 2 | 3 | {% block body_id 'admin_post_show' %} 4 | 5 | {% block main %} 6 |

{{ post.title }}

7 | 8 | 12 | 13 |
14 |

{{ 'label.summary'|trans }}: {{ post.summary }}

15 |
16 | 17 | {{ post.content|md2html }} 18 | 19 | {{ include('blog/_post_tags.html.twig') }} 20 | {% endblock %} 21 | 22 | {% block sidebar %} 23 | 28 | 29 |
30 | {{ include('admin/blog/_delete_form.html.twig', {post: post}, with_context = false) }} 31 |
32 | 33 | {{ parent() }} 34 | 35 | {{ show_source_code(_self) }} 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /templates/admin/layout.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | This is the base template of the all backend pages. Since this layout is similar 3 | to the global layout, we inherit from it to just change the contents of some 4 | blocks. In practice, backend templates are using a three-level inheritance, 5 | showing how powerful, yet easy to use, is Twig's inheritance mechanism. 6 | See https://symfony.com/doc/current/book/templating.html#template-inheritance-and-layouts 7 | #} 8 | {% extends 'base.html.twig' %} 9 | 10 | {% block stylesheets %} 11 | {{ parent() }} 12 | {{ encore_entry_link_tags('admin') }} 13 | {% endblock %} 14 | 15 | {% block javascripts %} 16 | {{ parent() }} 17 | {{ encore_entry_script_tags('admin') }} 18 | {% endblock %} 19 | 20 | {% block header_navigation_links %} 21 |
  • 22 | 23 | {{ 'menu.post_list'|trans }} 24 | 25 |
  • 26 |
  • 27 | 28 | {{ 'menu.back_to_blog'|trans }} 29 | 30 |
  • 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /templates/blog/_comment_form.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | By default, forms enable client-side validation. This means that you can't 3 | test the server-side validation errors from the browser. To temporarily 4 | disable this validation, add the 'novalidate' attribute: 5 | 6 | {{ form_start(form, {method: ..., action: ..., attr: {novalidate: 'novalidate'}}) }} 7 | #} 8 | 9 | {{ form_start(form, {method: 'POST', action: path('comment_new', {'postSlug': post.slug})}) }} 10 | {# instead of displaying form fields one by one, you can also display them 11 | all with their default options and styles just by calling to this function: 12 | 13 | {{ form_widget(form) }} 14 | #} 15 | 16 |
    17 | 18 | {{ 'title.add_comment'|trans }} 19 | 20 | 21 | {# Render any global form error (e.g. when a constraint on a public getter method failed) #} 22 | {{ form_errors(form) }} 23 | 24 |
    25 | {{ form_label(form.content, 'label.content', {label_attr: {class: 'hidden'}}) }} 26 | 27 | {# Render any errors for the "content" field (e.g. when a class property constraint failed) #} 28 | {{ form_errors(form.content) }} 29 | 30 | {{ form_widget(form.content, {attr: {rows: 10}}) }} 31 | {{ form_help(form.content) }} 32 |
    33 | 34 |
    35 | 38 |
    39 |
    40 | {{ form_end(form) }} 41 | -------------------------------------------------------------------------------- /templates/blog/_delete_post_confirmation.html.twig: -------------------------------------------------------------------------------- 1 | {# Bootstrap modal, see http://getbootstrap.com/javascript/#modals #} 2 | 20 | -------------------------------------------------------------------------------- /templates/blog/_post_tags.html.twig: -------------------------------------------------------------------------------- 1 | {% if not post.tags.empty %} 2 | 11 | {% endif %} 12 | 13 | -------------------------------------------------------------------------------- /templates/blog/_rss.html.twig: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /templates/blog/about.html.twig: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    4 | {{ 'help.app_description'|trans|raw }} 5 |

    6 |

    7 | {{ 'help.more_information'|trans|raw }} 8 |

    9 |
    10 |
    11 | 12 | {# it's not mandatory to set the timezone in localizeddate(). This is done to 13 | avoid errors when the 'intl' PHP extension is not available and the application 14 | is forced to use the limited "intl polyfill", which only supports UTC and GMT #} 15 | 16 | -------------------------------------------------------------------------------- /templates/blog/comment_form_error.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body_id 'comment_form_error' %} 4 | 5 | {% block main %} 6 |

    {{ 'title.comment_error'|trans }}

    7 | 8 |
    9 | {{ include('blog/_comment_form.html.twig') }} 10 |
    11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /templates/blog/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body_id 'blog_index' %} 4 | 5 | {% block main %} 6 | {% for post in paginator.results %} 7 |
    8 |

    9 | 10 | {{ post.title }} 11 | 12 |

    13 | 14 | 18 | 19 |

    {{ post.summary }}

    20 | 21 | {{ include('blog/_post_tags.html.twig') }} 22 |
    23 | {% else %} 24 |
    {{ 'post.no_posts_found'|trans }}
    25 | {% endfor %} 26 | 27 | {% if paginator.hasToPaginate %} 28 | 51 | {% endif %} 52 | {% endblock %} 53 | 54 | {% block sidebar %} 55 | {{ parent() }} 56 | 57 | {{ show_source_code(_self) }} 58 | {{ include('blog/_rss.html.twig') }} 59 | {% endblock %} 60 | -------------------------------------------------------------------------------- /templates/blog/index.xml.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ 'rss.title'|trans }} 5 | {{ 'rss.description'|trans }} 6 | {{ 'now'|date('r', timezone='GMT') }} 7 | {{ (paginator.results|last).publishedAt|default('now')|date('r', timezone='GMT') }} 8 | {{ url('blog_index') }} 9 | {{ app.request.locale }} 10 | 11 | {% for post in paginator.results %} 12 | 13 | {{ post.title }} 14 | {{ post.summary }} 15 | {{ url('blog_post', {'slug': post.slug}) }} 16 | {{ url('blog_post', {'slug': post.slug}) }} 17 | {{ post.publishedAt|date(format='r', timezone='GMT') }} 18 | {{ post.author.email }} 19 | {% for tag in post.tags %} 20 | {{ tag.name }} 21 | {% endfor %} 22 | 23 | {% endfor %} 24 | 25 | 26 | -------------------------------------------------------------------------------- /templates/blog/post_show.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body_id 'blog_post_show' %} 4 | 5 | {% block main %} 6 |

    {{ post.title }}

    7 | 8 | 12 | 13 | {{ post.content|md2html }} 14 | 15 | {{ include('blog/_post_tags.html.twig') }} 16 | 17 |
    18 | {# The 'IS_AUTHENTICATED_FULLY' role ensures that the user has entered 19 | their credentials (login + password) during this session. If they 20 | are automatically logged via the 'Remember Me' functionality, they won't 21 | be able to add a comment. 22 | See https://symfony.com/doc/current/security/remember_me.html#forcing-the-user-to-re-authenticate-before-accessing-certain-resources 23 | #} 24 | {% if is_granted('IS_AUTHENTICATED_FULLY') %} 25 | {{ render(controller('App\\Controller\\BlogController::commentForm', {'id': post.id})) }} 26 | {% else %} 27 |

    28 | 29 | {{ 'action.sign_in'|trans }} 30 | 31 | {{ 'post.to_publish_a_comment'|trans }} 32 |

    33 | {% endif %} 34 |
    35 | 36 |

    37 | {{ 'post.num_comments'|trans({ 'count': post.comments|length }) }} 38 |

    39 | 40 | {% for comment in post.comments %} 41 |
    42 | 43 |

    44 | {{ comment.author.fullName }} {{ 'post.commented_on'|trans }} 45 | {# it's not mandatory to set the timezone in localizeddate(). This is done to 46 | avoid errors when the 'intl' PHP extension is not available and the application 47 | is forced to use the limited "intl polyfill", which only supports UTC and GMT #} 48 | {{ comment.publishedAt|localizeddate('medium', 'short', null, 'UTC') }} 49 |

    50 |
    51 | {{ comment.content|md2html }} 52 |
    53 |
    54 | {% else %} 55 |
    56 |

    {{ 'post.no_comments'|trans }}

    57 |
    58 | {% endfor %} 59 | {% endblock %} 60 | 61 | {% block sidebar %} 62 | {% if is_granted('edit', post) %} 63 | 68 | {% endif %} 69 | 70 | {# the parent() function includes the contents defined by the parent template 71 | ('base.html.twig') for this block ('sidebar'). This is a very convenient way 72 | to share common contents in different templates #} 73 | {{ parent() }} 74 | 75 | {{ show_source_code(_self) }} 76 | {{ include('blog/_rss.html.twig') }} 77 | {% endblock %} 78 | -------------------------------------------------------------------------------- /templates/blog/search.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block javascripts %} 4 | {{ parent() }} 5 | {{ encore_entry_script_tags('search') }} 6 | {% endblock %} 7 | 8 | {% block body_id 'blog_search' %} 9 | 10 | {% block main %} 11 |
    12 |
    13 | 20 |
    21 |
    22 | 23 |
    24 |
    25 | {% endblock %} 26 | 27 | {% block sidebar %} 28 | {{ parent() }} 29 | 30 | {{ show_source_code(_self) }} 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /templates/bundles/TwigBundle/Exception/error.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | This template is used to render any error different from 403, 404 and 500. 3 | 4 | This is the simplest way to customize error pages in Symfony applications. 5 | In case you need it, you can also hook into the internal exception handling 6 | made by Symfony. This allows you to perform advanced tasks and even recover 7 | your application from some errors. 8 | See https://symfony.com/doc/current/cookbook/controller/error_pages.html 9 | #} 10 | 11 | {% extends 'base.html.twig' %} 12 | 13 | {% block body_id 'error' %} 14 | 15 | {% block main %} 16 |

    {{ 'http_error.name'|trans({ '%status_code%': status_code }) }}

    17 | 18 |

    19 | {{ 'http_error.description'|trans({ '%status_code%': status_code }) }} 20 |

    21 |

    22 | {{ 'http_error.suggestion'|trans({ '%url%': path('blog_index') })|raw }} 23 |

    24 | {% endblock %} 25 | 26 | {% block sidebar %} 27 | {{ parent() }} 28 | 29 | {{ show_source_code(_self) }} 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/bundles/TwigBundle/Exception/error403.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | This template is used to render errors of type HTTP 403 (Forbidden) 3 | 4 | This is the simplest way to customize error pages in Symfony applications. 5 | In case you need it, you can also hook into the internal exception handling 6 | made by Symfony. This allows you to perform advanced tasks and even recover 7 | your application from some errors. 8 | See https://symfony.com/doc/current/cookbook/controller/error_pages.html 9 | #} 10 | 11 | {% extends 'base.html.twig' %} 12 | 13 | {% block body_id 'error' %} 14 | 15 | {% block main %} 16 |

    {{ 'http_error.name'|trans({ '%status_code%': 403 }) }}

    17 | 18 |

    19 | {{ 'http_error_403.description'|trans }} 20 |

    21 |

    22 | {{ 'http_error_403.suggestion'|trans }} 23 |

    24 | {% endblock %} 25 | 26 | {% block sidebar %} 27 | {{ parent() }} 28 | 29 | {{ show_source_code(_self) }} 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/bundles/TwigBundle/Exception/error404.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | This template is used to render errors of type HTTP 404 (Not Found) 3 | 4 | This is the simplest way to customize error pages in Symfony applications. 5 | In case you need it, you can also hook into the internal exception handling 6 | made by Symfony. This allows you to perform advanced tasks and even recover 7 | your application from some errors. 8 | See https://symfony.com/doc/current/cookbook/controller/error_pages.html 9 | #} 10 | 11 | {% extends 'base.html.twig' %} 12 | 13 | {% block body_id 'error' %} 14 | 15 | {% block main %} 16 |

    {{ 'http_error.name'|trans({ '%status_code%': 404 }) }}

    17 | 18 |

    19 | {{ 'http_error_404.description'|trans }} 20 |

    21 |

    22 | {{ 'http_error_404.suggestion'|trans({ '%url%': path('blog_index') })|raw }} 23 |

    24 | {% endblock %} 25 | 26 | {% block sidebar %} 27 | {{ parent() }} 28 | 29 | {{ show_source_code(_self) }} 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/bundles/TwigBundle/Exception/error500.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | This template is used to render errors of type HTTP 500 (Internal Server Error) 3 | 4 | This is the simplest way to customize error pages in Symfony applications. 5 | In case you need it, you can also hook into the internal exception handling 6 | made by Symfony. This allows you to perform advanced tasks and even recover 7 | your application from some errors. 8 | See https://symfony.com/doc/current/cookbook/controller/error_pages.html 9 | #} 10 | 11 | {% extends 'base.html.twig' %} 12 | 13 | {% block stylesheets %} 14 | {{ parent() }} 15 | 16 | {% endblock %} 17 | 18 | {% block body_id 'error' %} 19 | 20 | {% block main %} 21 |

    {{ 'http_error.name'|trans({ '%status_code%': 500 }) }}

    22 | 23 |

    24 | {{ 'http_error_500.description'|trans }} 25 |

    26 |

    27 | {{ 'http_error_500.suggestion'|trans({ '%url%': path('blog_index') })|raw }} 28 |

    29 | {% endblock %} 30 | 31 | {% block sidebar %} 32 | {{ parent() }} 33 | 34 | {{ show_source_code(_self) }} 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /templates/debug/source_code.html.twig: -------------------------------------------------------------------------------- 1 |
    2 |

    3 | {{ 'help.show_code'|trans|raw }} 4 |

    5 | 6 | 9 | 10 | 35 |
    36 | -------------------------------------------------------------------------------- /templates/default/_flash_messages.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | This is a template fragment designed to be included in other templates 3 | See https://symfony.com/doc/current/book/templating.html#including-other-templates 4 | 5 | A common practice to better distinguish between templates and fragments is to 6 | prefix fragments with an underscore. That's why this template is called 7 | '_flash_messages.html.twig' instead of 'flash_messages.html.twig' 8 | #} 9 | 10 | {# 11 | The request method check is needed to prevent starting the session when looking for "flash messages": 12 | https://symfony.com/doc/current/session/avoid_session_start.html 13 | 14 | TIP: With FOSHttpCache you can also adapt this to make it cache safe: 15 | https://foshttpcachebundle.readthedocs.io/en/latest/features/helpers/flash-message.html 16 | #} 17 | {% if app.request.hasPreviousSession %} 18 |
    19 | {% for type, messages in app.flashes %} 20 | {% for message in messages %} 21 | {# Bootstrap alert, see http://getbootstrap.com/components/#alerts #} 22 | 29 | {% endfor %} 30 | {% endfor %} 31 |
    32 | {% endif %} 33 | -------------------------------------------------------------------------------- /templates/default/homepage.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body_id 'homepage' %} 4 | 5 | {# 6 | the homepage is a special page which displays neither a header nor a footer. 7 | this is done with the 'trick' of defining empty Twig blocks without any content 8 | #} 9 | {% block header %}{% endblock %} 10 | {% block footer %}{% endblock %} 11 | 12 | {% block body %} 13 | 16 | 17 |
    18 |
    19 |
    20 |

    21 | {{ 'help.browse_app'|trans|raw }} 22 |

    23 |

    24 | 25 | {{ 'action.browse_app'|trans }} 26 | 27 |

    28 |
    29 |
    30 | 31 |
    32 |
    33 |

    34 | {{ 'help.browse_admin'|trans|raw }} 35 |

    36 |

    37 | 38 | {{ 'action.browse_admin'|trans }} 39 | 40 |

    41 |
    42 |
    43 |
    44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /templates/form/fields.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | Each field type is rendered by a template fragment, which is determined 3 | by the name of your form type class (DateTimePickerType -> date_time_picker) 4 | and the suffix "_widget". This can be controlled by overriding getBlockPrefix() 5 | in DateTimePickerType. 6 | 7 | See https://symfony.com/doc/current/cookbook/form/create_custom_field_type.html#creating-a-template-for-the-field 8 | #} 9 | 10 | {% block date_time_picker_widget %} 11 |
    12 | {{ block('datetime_widget') }} 13 | 14 | 15 | 16 |
    17 | {% endblock %} 18 | 19 | {% block tags_input_widget %} 20 |
    21 | {{ form_widget(form, {'attr': {'data-toggle': 'tagsinput', 'data-tags': tags|json_encode}}) }} 22 | 23 | 24 | 25 |
    26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /templates/form/layout.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'bootstrap_3_layout.html.twig' %} 2 | 3 | {# Errors #} 4 | 5 | {% block form_errors -%} 6 | {% if errors|length > 0 -%} 7 | {% if form is not rootform %}{% else %}
    {% endif %} 8 |
      9 | {%- for error in errors -%} 10 | {# use font-awesome icon library #} 11 |
    • {{ error.message }}
    • 12 | {%- endfor -%} 13 |
    14 | {% if form is not rootform %}{% else %}
    {% endif %} 15 | {%- endif %} 16 | {%- endblock form_errors %} 17 | -------------------------------------------------------------------------------- /templates/user/change_password.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body_id 'user_password' %} 4 | 5 | {% block main %} 6 |

    {{ 'title.change_password'|trans }}

    7 | 8 | 9 | 10 | {{ form_start(form) }} 11 | {{ form_widget(form) }} 12 | 13 | 16 | {{ form_end(form) }} 17 | {% endblock %} 18 | 19 | {% block sidebar %} 20 | 25 | 26 | {{ parent() }} 27 | 28 | {{ show_source_code(_self) }} 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /templates/user/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body_id 'user_edit' %} 4 | 5 | {% block main %} 6 |

    {{ 'title.edit_user'|trans }}

    7 | 8 | {{ form_start(form) }} 9 | {{ form_widget(form) }} 10 | 11 | 14 | {{ form_end(form) }} 15 | {% endblock %} 16 | 17 | {% block sidebar %} 18 | 23 | 24 | {{ parent() }} 25 | 26 | {{ show_source_code(_self) }} 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /tests/_data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/tests/_data/.gitkeep -------------------------------------------------------------------------------- /tests/_output/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/_support/AcceptanceTester.php: -------------------------------------------------------------------------------- 1 | amOnPage("/{$this->locale}$url"); 33 | } 34 | 35 | /** 36 | * Define custom actions here 37 | */ 38 | } 39 | -------------------------------------------------------------------------------- /tests/_support/Helper/Acceptance.php: -------------------------------------------------------------------------------- 1 | wantTo('open blog page and see article there'); 6 | $I->amOnPage('/'); 7 | $I->click('Browse application'); 8 | $I->seeInCurrentUrl('blog'); 9 | $I->seeElement('article.post'); 10 | -------------------------------------------------------------------------------- /tests/acceptance/LoginCept.php: -------------------------------------------------------------------------------- 1 | wantTo('login as admin to backend'); 6 | $I->amOnPage('/'); 7 | $I->click('Browse backend'); 8 | $I->seeInCurrentUrl('/en/login'); 9 | $I->see('Secure Sign in', 'legend'); 10 | $I->fillField('Username', 'jane_admin'); 11 | $I->fillField('Password', 'kitten'); 12 | $I->click('Sign in'); 13 | $I->seeInCurrentUrl('admin'); 14 | $I->seeLink('Logout'); 15 | -------------------------------------------------------------------------------- /tests/functional.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for functional tests 4 | # Emulate web requests and make application process them 5 | # Include one of framework modules (Symfony2, Yii2, Laravel5) to use it 6 | # Remove this suite if you don't use frameworks 7 | 8 | actor: FunctionalTester 9 | modules: 10 | enabled: 11 | - Symfony: 12 | app_path: 'src' 13 | environment: 'test' 14 | - Doctrine2: 15 | depends: Symfony 16 | cleanup: true 17 | - Asserts 18 | - Sequence 19 | - \App\Tests\Helper\Functional 20 | -------------------------------------------------------------------------------- /tests/functional/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/tests/functional/.gitkeep -------------------------------------------------------------------------------- /tests/functional/AuthCest.php: -------------------------------------------------------------------------------- 1 | amOnLocalizedPage('/login'); 11 | } 12 | 13 | /** 14 | * @group user 15 | * @param FunctionalTester $I 16 | */ 17 | public function authAsUser(FunctionalTester $I) 18 | { 19 | $I->fillField('Username', 'john_user'); 20 | $I->fillField('Password', 'kitten'); 21 | $I->click('Sign in'); 22 | $I->amOnLocalizedPage('/blog'); 23 | $I->seeLink('Logout'); 24 | 25 | $I->expect("user can't access admin area"); 26 | $I->amOnLocalizedPage('/admin/post/'); 27 | $I->seeResponseCodeIs(403); 28 | 29 | } 30 | 31 | /** 32 | * @param FunctionalTester $I 33 | */ 34 | public function authAsAdmin(FunctionalTester $I) 35 | { 36 | $I->fillField('Username', 'jane_admin'); 37 | $I->fillField('Password', 'kitten'); 38 | $I->click('Sign in'); 39 | $I->amOnLocalizedPage('/admin/post/'); 40 | $I->see('Post List', 'h1'); 41 | $I->seeLink('Logout'); 42 | } 43 | 44 | public function invalidAuth(FunctionalTester $I) 45 | { 46 | $I->fillField('Username', 'notauser'); 47 | $I->fillField('Password', 'kitten'); 48 | $I->click('Sign in'); 49 | $I->see('Invalid credentials', '.alert'); 50 | $I->seeInCurrentUrl('/login'); 51 | $I->dontSeeLink('Logout'); 52 | } 53 | } -------------------------------------------------------------------------------- /tests/functional/BlogCest.php: -------------------------------------------------------------------------------- 1 | amOnPage('/en/blog/'); 11 | $I->seeNumberOfElements('article.post', Post::NUM_ITEMS); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/functional/CommentCest.php: -------------------------------------------------------------------------------- 1 | amOnLocalizedPage('/login'); 15 | $I->submitForm('#main form', ['_username' => 'jane_admin', '_password' => 'kitten']); 16 | $I->seeResponseCodeIs(HttpCode::OK); 17 | $I->seeCurrentRouteIs('admin_index'); 18 | $I->see('Post List'); 19 | $I->click('Create a new post'); 20 | $I->seeResponseCodeIs(HttpCode::OK); 21 | $I->seeCurrentRouteIs('admin_post_new'); 22 | $I->see('Post creation'); 23 | $I->fillField('Title', 'Hi, Symfony!'); 24 | $I->fillField('Summary', 'Hi, Symfony, Summary!'); 25 | $I->fillField('Content', 'Hi, Symfony, Content!'); 26 | $I->click('Create post'); 27 | 28 | $I->see('Post created successfully!'); 29 | 30 | $I->seeInRepository(Post::class, [ 31 | 'content' => 'Hi, Symfony, Content!', 32 | 'author' => [ 33 | 'username' => 'jane_admin', 34 | ], 35 | ]); 36 | } 37 | 38 | public function createComment(FunctionalTester $I) 39 | { 40 | $I->amOnLocalizedPage('/login'); 41 | $I->submitForm('#main form', ['_username' => 'jane_admin', '_password' => 'kitten']); 42 | $I->seeResponseCodeIs(HttpCode::OK); 43 | $I->seeCurrentRouteIs('admin_index'); 44 | 45 | $I->amOnPage('/en/blog/posts/lorem-ipsum-dolor-sit-amet-consectetur-adipiscing-elit'); 46 | $I->seeResponseCodeIs(HttpCode::OK); 47 | $I->seeCurrentRouteIs('blog_post'); 48 | $I->see('Add a comment'); 49 | $I->fillField('comment[content]', 'Hi, Symfony!'); 50 | $I->click('Publish comment'); 51 | $I->seeCurrentRouteIs('blog_post'); 52 | $I->see('Hi, Symfony!'); 53 | // This particular assertion is a bit of overkill, but it also serves as a testcase 54 | // of `seeInRepository` method with complex associations. Here is how this assertion 55 | // translates to English: 56 | // 57 | // * I see a comment entity with content equal to 'Hi, Symfony!' 58 | // * and 'author' association points to a user entity with username equal to 'john_user' 59 | // ** note: john_user is one who created the comment, as it is used in login 60 | // procedure above 61 | // * and 'post' association points to a post entity which... 62 | // * ...has an associated comment entity which... 63 | // * ...has same content and author with same username. 64 | $I->seeInRepository(Comment::class, [ 65 | 'content' => 'Hi, Symfony!', 66 | 'author' => [ 67 | 'username' => 'jane_admin', 68 | ], 69 | 'post' => [ 70 | 'comments' => [ 71 | 'content' => 'Hi, Symfony!', 72 | 'author' => [ 73 | 'username' => 'jane_admin' 74 | ] 75 | ] 76 | ] 77 | ]); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/functional/PublicUrlsCept.php: -------------------------------------------------------------------------------- 1 | am('Anonymous'); 5 | $I->wantTo('Open public urls and see requested page'); 6 | 7 | $publicUrls = [ 8 | '/', 9 | '/en/blog/', 10 | '/en/login', 11 | ]; 12 | 13 | foreach ($publicUrls as $url) { 14 | $I->amOnPage($url); 15 | $I->seeResponseCodeIs(200); 16 | $I->seeCurrentUrlEquals($url); 17 | } 18 | -------------------------------------------------------------------------------- /tests/functional/SecureUrlsCept.php: -------------------------------------------------------------------------------- 1 | am('Anonymous'); 5 | $I->wantTo('Open secure urls and see login page'); 6 | 7 | $secureUrls = [ 8 | '/en/admin/post/', 9 | '/en/admin/post/new', 10 | '/en/admin/post/1', 11 | '/en/admin/post/1/edit', 12 | ]; 13 | 14 | foreach ($secureUrls as $url) { 15 | $I->amOnPage($url); 16 | $I->seeResponseCodeIs(200); 17 | $I->seeCurrentUrlEquals('/en/login'); 18 | } 19 | -------------------------------------------------------------------------------- /tests/functional/ServiceContainerCest.php: -------------------------------------------------------------------------------- 1 | grabService($example[0]); 30 | 31 | $I->assertInstanceOf($example[1], $service); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/functional/SubdomainIndexCept.php: -------------------------------------------------------------------------------- 1 | wantTo('test subdomain route'); 4 | 5 | $I->amOnPage('http://test.example.com/es/domain'); 6 | $I->see('SUBDOMAIN test'); 7 | -------------------------------------------------------------------------------- /tests/functional/UnknownSubdomainThrowsExceptionCept.php: -------------------------------------------------------------------------------- 1 | wantTo('get exception on unknown subdomain route'); 4 | 5 | try { 6 | $I->amOnPage('http://test.example2.com'); 7 | $I->fail('ExternalUrlException wasn\'t thrown'); 8 | } catch (Codeception\Exception\ExternalUrlException $e) { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /tests/unit.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for unit or integration tests. 4 | 5 | actor: UnitTester 6 | modules: 7 | enabled: 8 | - Asserts 9 | - \App\Tests\Helper\Unit 10 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/Utils/SluggerTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Tests\Utils; 13 | 14 | use App\Utils\Slugger; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | /** 18 | * Unit test for the application utils. 19 | * 20 | * See https://symfony.com/doc/current/book/testing.html#unit-tests 21 | * 22 | * Execute the application tests using this command (requires PHPUnit to be installed): 23 | * 24 | * $ cd your-symfony-project/ 25 | * $ ./vendor/bin/phpunit 26 | */ 27 | class SluggerTest extends TestCase 28 | { 29 | /** 30 | * @dataProvider getSlugs 31 | */ 32 | public function testSlugify(string $string, string $slug) 33 | { 34 | $this->assertSame($slug, Slugger::slugify($string)); 35 | } 36 | 37 | public function getSlugs() 38 | { 39 | yield ['Lorem Ipsum', 'lorem-ipsum']; 40 | yield [' Lorem Ipsum ', 'lorem-ipsum']; 41 | yield [' lOrEm iPsUm ', 'lorem-ipsum']; 42 | yield ['!Lorem Ipsum!', '!lorem-ipsum!']; 43 | yield ['lorem-ipsum', 'lorem-ipsum']; 44 | yield ['lorem 日本語 ipsum', 'lorem-日本語-ipsum']; 45 | yield ['lorem русский язык ipsum', 'lorem-русский-язык-ipsum']; 46 | yield ['lorem العَرَبِيَّة‎‎ ipsum', 'lorem-العَرَبِيَّة‎‎-ipsum']; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/unit/Utils/ValidatorTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Tests\Utils; 13 | 14 | use App\Utils\Validator; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class ValidatorTest extends TestCase 18 | { 19 | private $object; 20 | 21 | public function __construct() 22 | { 23 | parent::__construct(); 24 | 25 | $this->object = new Validator(); 26 | } 27 | 28 | public function testValidateUsername() 29 | { 30 | $test = 'username'; 31 | 32 | $this->assertSame($test, $this->object->validateUsername($test)); 33 | } 34 | 35 | public function testValidateUsernameEmpty() 36 | { 37 | $this->expectException('Exception'); 38 | $this->expectExceptionMessage('The username can not be empty.'); 39 | $this->object->validateUsername(null); 40 | } 41 | 42 | public function testValidateUsernameInvalid() 43 | { 44 | $this->expectException('Exception'); 45 | $this->expectExceptionMessage('The username must contain only lowercase latin characters and underscores.'); 46 | $this->object->validateUsername('INVALID'); 47 | } 48 | 49 | public function testValidatePassword() 50 | { 51 | $test = 'password'; 52 | 53 | $this->assertSame($test, $this->object->validatePassword($test)); 54 | } 55 | 56 | public function testValidatePasswordEmpty() 57 | { 58 | $this->expectException('Exception'); 59 | $this->expectExceptionMessage('The password can not be empty.'); 60 | $this->object->validatePassword(null); 61 | } 62 | 63 | public function testValidatePasswordInvalid() 64 | { 65 | $this->expectException('Exception'); 66 | $this->expectExceptionMessage('The password must be at least 6 characters long.'); 67 | $this->object->validatePassword('12345'); 68 | } 69 | 70 | public function testValidateEmail() 71 | { 72 | $test = '@'; 73 | 74 | $this->assertSame($test, $this->object->validateEmail($test)); 75 | } 76 | 77 | public function testValidateEmailEmpty() 78 | { 79 | $this->expectException('Exception'); 80 | $this->expectExceptionMessage('The email can not be empty.'); 81 | $this->object->validateEmail(null); 82 | } 83 | 84 | public function testValidateEmailInvalid() 85 | { 86 | $this->expectException('Exception'); 87 | $this->expectExceptionMessage('The email should look like a real email.'); 88 | $this->object->validateEmail('invalid'); 89 | } 90 | 91 | public function testValidateFullName() 92 | { 93 | $test = 'Full Name'; 94 | 95 | $this->assertSame($test, $this->object->validateFullName($test)); 96 | } 97 | 98 | public function testValidateFullNameEmpty() 99 | { 100 | $this->expectException('Exception'); 101 | $this->expectExceptionMessage('The full name can not be empty.'); 102 | $this->object->validateFullName(null); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.bg.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Напишете обобщение на публикацията си! 8 | 9 | 10 | post.blank_content 11 | Публикацията трябва да има съдържание! 12 | 13 | 14 | post.too_short_content 15 | Съдержанието на публикацията е прекалено малко ({ limit } минимум символа) 16 | 17 | 18 | post.too_many_tags 19 | Прекалено много тагове (добави { limit } тага или по-малко) 20 | 21 | 22 | comment.blank 23 | Моля не оставяйте коментара празен! 24 | 25 | 26 | comment.too_short 27 | Коментара е пркалено кратък ({ limit } симвала минимум) 28 | 29 | 30 | comment.too_long 31 | Коментара е прекалено дълъг ({ limit } симвала максимум) 32 | 33 | 34 | comment.is_spam 35 | Съдържанието на коментара се разглежда като спам. 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.ca.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | No és possible deixar buit el resum de l'article. 8 | 9 | 10 | post.blank_content 11 | No és possible deixar buit el contingut de l'article. 12 | 13 | 14 | post.too_short_content 15 | El contingut de l'article és massa curt ({ limit } caràcters com a mínim) 16 | 17 | 18 | comment.blank 19 | No és possible deixar buit el contingut del comentari. 20 | 21 | 22 | comment.too_short 23 | El comentari és massa curt ({ limit } caràcters com a mínim) 24 | 25 | 26 | comment.too_long 27 | El comentari és massa llarg ({ limit } caràcters com a màxim) 28 | 29 | 30 | comment.is_spam 31 | El contingut del comentari es considera spam. 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.cs.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Napište shrnutí příspěvku! 8 | 9 | 10 | post.too_short_content 11 | Příspěvek je příliš krátký (musí mít minimálně { limit } znak)|Příspěvek je příliš krátký (musí mít minimálně { limit } znaky)|Příspěvek je příliš krátký (musí mít minimálně { limit } znaků) 12 | 13 | 14 | comment.blank 15 | Prosím, napište text komentáře! 16 | 17 | 18 | comment.too_short 19 | Komentář je příliš krátký (musí mít minimálně { limit } znak)|Komentář je příliš krátký (musí mít minimálně { limit } znaky)|Komentář je příliš krátký (musí mít minimálně { limit } znaků) 20 | 21 | 22 | comment.too_long 23 | Komentář je příliš dlouhý (musí mít maximálně { limit } znak)|Komentář je příliš dlouhý (musí mít maximálně { limit } znaky)|Komentář je příliš dlouhý (musí mít maximálně { limit } znaků) 24 | 25 | 26 | comment.is_spam 27 | Obsah tohoto komentáře je považován za spam. 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.de.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Gib deinem Beitrag eine Zusammenfassung! 8 | 9 | 10 | post.blank_content 11 | Dein Beitrag sollte einen Inhalt haben! 12 | 13 | 14 | post.too_short_content 15 | Der Beitragsinhalt ist zu kurz (mindestens { limit } Zeichen) 16 | 17 | 18 | comment.blank 19 | Bitte gib einen Kommentar ein! 20 | 21 | 22 | comment.too_short 23 | Der Kommentar ist zu kurz (mindestens { limit } Zeichen) 24 | 25 | 26 | comment.too_long 27 | Der Kommentar ist zu lang (maximal { limit } Zeichen) 28 | 29 | 30 | comment.is_spam 31 | Der Inhalt des Kommentars wird als Spam eingestuft. 32 | 33 | 34 | post.too_many_tags 35 | Zu viele Tags (höchstens { limit } Tags sind erlaubt) 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.en.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Give your post a summary! 8 | 9 | 10 | post.blank_content 11 | Your post should have some content! 12 | 13 | 14 | post.too_short_content 15 | Post content is too short ({ limit } characters minimum) 16 | 17 | 18 | post.too_many_tags 19 | Too many tags (add { limit } tags or less) 20 | 21 | 22 | comment.blank 23 | Please don't leave your comment blank! 24 | 25 | 26 | comment.too_short 27 | Comment is too short ({ limit } characters minimum) 28 | 29 | 30 | comment.too_long 31 | Comment is too long ({ limit } characters maximum) 32 | 33 | 34 | comment.is_spam 35 | The content of this comment is considered spam. 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.es.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | No es posible dejar el resumen del artículo vacío. 8 | 9 | 10 | post.blank_content 11 | No es posible dejar el contenido del artículo vacío. 12 | 13 | 14 | post.too_short_content 15 | El contenido del artículo es demasiado corto ({ limit } caracteres como mínimo) 16 | 17 | 18 | post.too_many_tags 19 | Demasiadas etiquetas (añade { limit } como máximo) 20 | 21 | 22 | comment.blank 23 | No es posible dejar el contenido del comentario vacío. 24 | 25 | 26 | comment.too_short 27 | El comentario es demasiado corto ({ limit } caracteres como mínimo) 28 | 29 | 30 | comment.too_long 31 | El comentario es demasiado largo ({ limit } caracteres como máximo) 32 | 33 | 34 | comment.is_spam 35 | El contenido del comentario se considera spam. 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.fr.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Veuillez donner un résumé à votre post. 8 | 9 | 10 | post.blank_content 11 | Veuillez donner un contenu à votre post. 12 | 13 | 14 | post.too_short_content 15 | Le contenu de votre post est trop court ({ limit } caractères minimum) 16 | 17 | 18 | comment.blank 19 | Veuillez ne pas laisser votre commentaire vide. 20 | 21 | 22 | comment.too_short 23 | Votre commentaire est trop court ({ limit } caractères minimum) 24 | 25 | 26 | comment.too_long 27 | Votre commentaire est trop long ({ limit } caractères maximum) 28 | 29 | 30 | comment.is_spam 31 | Le contenu de votre commentaire est considéré comme du spam. 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.hr.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Dodajte sažetak svojem članku! 8 | 9 | 10 | post.blank_content 11 | Vaš članak treba imati sadržaj! 12 | 13 | 14 | post.too_short_content 15 | Sadržaj članka je prekratak (koristiti minimalno { limit } slova ili simbola) 16 | 17 | 18 | post.too_many_tags 19 | Previše oznaka (dodaj najviše { limit } oznaka ili manje) 20 | 21 | 22 | comment.blank 23 | Molim ne ostavljajte svoj komentar praznim! 24 | 25 | 26 | comment.too_short 27 | Komentar je prekratak (potrebno je minimalno { limit } slova ili simbola) 28 | 29 | 30 | comment.too_long 31 | Komentar je predugačak (koristiti maksimalno { limit } slova ili simbola) 32 | 33 | 34 | comment.is_spam 35 | Sadržaj ovog komentara smatra se spam sadržajem. 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.id.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Beri posting anda ringkasan! 8 | 9 | 10 | post.blank_content 11 | Posting anda harus mempunyai konten! 12 | 13 | 14 | post.too_short_content 15 | Konten terlalu singkat (Minimal { limit } karakter) 16 | 17 | 18 | comment.blank 19 | Mohon jangan tinggalkan komentar kosong! 20 | 21 | 22 | comment.too_short 23 | Komentar terlalu singkat (Minimal { limit } karakter) 24 | 25 | 26 | comment.too_long 27 | Komentar terlalu panjang (Maksimal { limit } karakter) 28 | 29 | 30 | comment.is_spam 31 | Konten komentar ini dianggap sebagai spam. 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.it.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Da' una descrizione al tuo post! 8 | 9 | 10 | post.blank_content 11 | Da' un contenuto al tuo post! 12 | 13 | 14 | post.too_short_content 15 | Il contenuto del post è troppo breve (minimo { limit } caratteri) 16 | 17 | 18 | comment.blank 19 | Per favore non lasciare in bianco il tuo commento! 20 | 21 | 22 | comment.too_short 23 | Il commento è troppo breve (minimo { limit } caratteri) 24 | 25 | 26 | comment.too_long 27 | Il commento è troppo lungo (massimo { limit } caratteri) 28 | 29 | 30 | comment.is_spam 31 | Il contenuto di questo commento è considerato come spam. 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.ja.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | 要約を入力してください。 8 | 9 | 10 | post.blank_content 11 | 本文を入力してください。 12 | 13 | 14 | post.too_short_content 15 | 本文が短すぎます ({ limit } 文字以上必要です) 16 | 17 | 18 | comment.blank 19 | コメントを入力してください。 20 | 21 | 22 | comment.too_short 23 | コメントが短すぎます ({ limit } 文字以上必要です) 24 | 25 | 26 | comment.too_long 27 | コメントが長すぎます ({ limit } 文字以下にしてください) 28 | 29 | 30 | comment.is_spam 31 | コメントの内容がスパムと判定されました。 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.lt.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Įrašo santrauka negali būti tuščia 8 | 9 | 10 | post.blank_content 11 | Įrašo turinys negali būti tuščias 12 | 13 | 14 | post.too_short_content 15 | Per trumpas įrašo turinys (nesiekia { limit } simbolių) 16 | 17 | 18 | post.too_many_tags 19 | Per daug žymų (viršyja { limit }) 20 | 21 | 22 | comment.blank 23 | Komentaras negali būti tuščias 24 | 25 | 26 | comment.too_short 27 | Per trumpas komentaras (nesiekia { limit } simbolių) 28 | 29 | 30 | comment.too_long 31 | Per ilgas komentaras (viršyja { limit } simbolių) 32 | 33 | 34 | comment.is_spam 35 | Komentaras traktuojamas kaip brukalas. 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.nl.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Geef uw bericht een samenvatting. 8 | 9 | 10 | post.blank_content 11 | Uw bericht heeft nog geen inhoud. 12 | 13 | 14 | post.too_short_content 15 | Bericht inhoud is te kort (minimaal { limit } karakters) 16 | 17 | 18 | comment.blank 19 | Vul alstublieft een reactie in. 20 | 21 | 22 | comment.too_short 23 | Reactie is te kort (minimaal { limit } karakters) 24 | 25 | 26 | comment.too_long 27 | Reactie is te lang (maximaal { limit } karakters) 28 | 29 | 30 | comment.is_spam 31 | De inhoud van deze reactie wordt als spam gemarkeerd. 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.pl.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Dodaj podsumowanie Twojego artykułu! 8 | 9 | 10 | post.blank_content 11 | Treść artykułu nie może być pusta! 12 | 13 | 14 | post.too_short_content 15 | Treść artykułu jest za krótka (minimum: { limit } znak)|Treść artykułu jest za krótka (minimum: { limit } znaki)|Treść artykułu jest za krótka (minimum: { limit } znaków) 16 | 17 | 18 | comment.blank 19 | Pole komentarza nie może być puste! 20 | 21 | 22 | comment.too_short 23 | Twój komentarz jest za krótki (minimum: { limit } znak)|Twój komentarz jest za krótki (minimum: { limit } znaki)|Twój komentarz jest za krótki (minimum: { limit } znaków) 24 | 25 | 26 | comment.too_long 27 | Twój komentarz jest za długi (maksimum: { limit } znak)|Twój komentarz jest za długi (maksimum: { limit } znaki)|Twój komentarz jest za długi (maksimum: { limit } znaków) 28 | 29 | 30 | comment.is_spam 31 | Twój komentarz został uznany za spam. 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.pt_BR.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Informe um sumário para o seu post! 8 | 9 | 10 | post.blank_content 11 | Informe um conteúdo para o seu post! 12 | 13 | 14 | post.too_short_content 15 | O conteúdo do post está muito curto (mínimo de { limit } caracteres) 16 | 17 | 18 | post.too_many_tags 19 | Tags demais (adicione { limit } tags ou menos) 20 | 21 | 22 | comment.blank 23 | Por favor, não deixe seu comentário vazio! 24 | 25 | 26 | comment.too_short 27 | O comentário está muito curto (mínimo de { limit } caracteres) 28 | 29 | 30 | comment.too_long 31 | O comentário está muito grande (máximo de { limit } caracteres) 32 | 33 | 34 | comment.is_spam 35 | O conteúdo desse comentário é considerado spam. 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.ro.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Dă articolului tău un rezumat! 8 | 9 | 10 | post.blank_content 11 | Articolul ar trebui să aibe conținut! 12 | 13 | 14 | post.too_short_content 15 | Conţinutul articolului este prea scurt (minimum { limit } caractere) 16 | 17 | 18 | comment.blank 19 | Te rugăm nu lăsa comentariul tău necompletat! 20 | 21 | 22 | comment.too_short 23 | Comentariul este prea scurt (minimum { limit } caractere) 24 | 25 | 26 | comment.too_long 27 | Comentariul este prea lung (maximum { limit } caractere) 28 | 29 | 30 | comment.is_spam 31 | Conţinutul acestui comentariu este considerat spam. 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.ru.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Введите краткое содержание вашей записи! 8 | 9 | 10 | post.blank_content 11 | Ваша запись должна содержать хоть какое-то содержание! 12 | 13 | 14 | post.too_short_content 15 | Содержание записи слишком короткое (минимум { limit } символов). 16 | 17 | 18 | post.too_many_tags 19 | Слишком много тегов (добавьте { limit } тегов или меньше) 20 | 21 | 22 | comment.blank 23 | Пожалуйста, не оставляйте текст комментария пустым! 24 | 25 | 26 | comment.too_short 27 | Комментарий слишком короткий, (минимум { limit } символов). 28 | 29 | 30 | comment.too_long 31 | Комментарий слишком длинный, (максимум { limit } символов). 32 | 33 | 34 | comment.is_spam 35 | Содержание этого комментария было расценено как спам. 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.sl.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Dodajte vaši objavi povzetek! 8 | 9 | 10 | post.blank_content 11 | Vaša objava mora imeti nekaj vsebine! 12 | 13 | 14 | post.too_short_content 15 | Vsebina objave je prekratka (vsaj { limit } znakov) 16 | 17 | 18 | post.too_many_tags 19 | Preveč značk (dodajte { limit } ali manj značk) 20 | 21 | 22 | comment.blank 23 | Ne pustite vašega komentarja praznega! 24 | 25 | 26 | comment.too_short 27 | Komentar je prekratek (vsaj { limit } znakov) 28 | 29 | 30 | comment.too_long 31 | Komentar je predolg (največ { limit } znakov) 32 | 33 | 34 | comment.is_spam 35 | Vsebina tega komentarja se smatra za spam. 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.tr.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Gönderiniz için bir özet giriniz! 8 | 9 | 10 | post.blank_content 11 | Gönderiniz bir içeriğe sahip olmalı! 12 | 13 | 14 | post.too_short_content 15 | Yayın içeriği çok kısa ({ limit } minimum karakter) 16 | 17 | 18 | post.too_many_tags 19 | Çok fazla etiket var ({ limit } etiketini veya daha azını ekleyin) 20 | 21 | 22 | comment.blank 23 | Lütfen yorumunuzu boş bırakmayın! 24 | 25 | 26 | comment.too_short 27 | Yorum çok kısa ({ limit } minimum karakter) 28 | 29 | 30 | comment.too_long 31 | Yorum çok uzun ({ limit } maksimum karakter) 32 | 33 | 34 | comment.is_spam 35 | Bu yorumun içeriği spam olarak kabul edilir. 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.uk.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | Введіть короткий зміст вашого запису! 8 | 9 | 10 | post.blank_content 11 | Ваш запис повинен містити хоч якийсь зміст! 12 | 13 | 14 | post.too_short_content 15 | Зміст запису занадто короткий (мінімум { limit } символів). 16 | 17 | 18 | post.too_many_tags 19 | Занадто багато тегів (додайте { limit } тегів або менше) 20 | 21 | 22 | comment.blank 23 | Будь ласка, не залишайте текст коментаря порожнім! 24 | 25 | 26 | comment.too_short 27 | Коментар занадто короткий, (мінімум { limit } символів). 28 | 29 | 30 | comment.too_long 31 | Коментар занадто довгий, (максимум { limit } символів). 32 | 33 | 34 | comment.is_spam 35 | Зміст цього коментаря було розцінено як спам. 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /translations/validators+intl-icu.zh_CN.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | post.blank_summary 7 | 请填写文章摘要! 8 | 9 | 10 | post.blank_content 11 | 请填写文章内容! 12 | 13 | 14 | post.too_short_content 15 | 文章内容太少 最少 ({ limit } 个字 ) 16 | 17 | 18 | post.too_many_tags 19 | 标签太多 (最多 { limit } 个标签) 20 | 21 | 22 | comment.blank 23 | 评论内容不能为空! 24 | 25 | 26 | comment.too_short 27 | 评论内容太少 (最少 { limit } 个字) 28 | 29 | 30 | comment.too_long 31 | 评论内容太多 (最多 { limit } 个字) 32 | 33 | 34 | comment.is_spam 35 | 非法评论. 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /var/data/blog.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/var/data/blog.sqlite -------------------------------------------------------------------------------- /var/data/blog_test.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/var/data/blog_test.sqlite -------------------------------------------------------------------------------- /var/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/var/log/.gitkeep -------------------------------------------------------------------------------- /var/sessions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeception/symfony-demo/173bd16e1f60a39c18a144f39d013f08a221ab73/var/sessions/.gitkeep -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // This project uses "Yarn" package manager for managing JavaScript dependencies along 2 | // with "Webpack Encore" library that helps working with the CSS and JavaScript files 3 | // that are stored in the "assets/" directory. 4 | // 5 | // Read https://symfony.com/doc/current/frontend.html to learn more about how 6 | // to manage CSS and JavaScript files in Symfony applications. 7 | var Encore = require('@symfony/webpack-encore'); 8 | 9 | Encore 10 | .setOutputPath('public/build/') 11 | .setPublicPath('/build') 12 | .cleanupOutputBeforeBuild() 13 | .autoProvidejQuery() 14 | .autoProvideVariables({ 15 | "window.Bloodhound": require.resolve('bloodhound-js'), 16 | "jQuery.tagsinput": "bootstrap-tagsinput" 17 | }) 18 | .enableSassLoader() 19 | // when versioning is enabled, each filename will include a hash that changes 20 | // whenever the contents of that file change. This allows you to use aggressive 21 | // caching strategies. Use Encore.isProduction() to enable it only for production. 22 | .enableVersioning(false) 23 | .addEntry('app', './assets/js/app.js') 24 | .addEntry('login', './assets/js/login.js') 25 | .addEntry('admin', './assets/js/admin.js') 26 | .addEntry('search', './assets/js/search.js') 27 | .splitEntryChunks() 28 | .enableSingleRuntimeChunk() 29 | .enableIntegrityHashes(Encore.isProduction()) 30 | .configureBabel(null, { 31 | useBuiltIns: 'usage', 32 | corejs: 3, 33 | }) 34 | ; 35 | 36 | module.exports = Encore.getWebpackConfig(); 37 | --------------------------------------------------------------------------------