├── .dockerignore ├── .editorconfig ├── .env ├── .env.test ├── .gitignore ├── .php_cs.dist ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── 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 ├── 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 ├── docker └── 000-default.conf ├── 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 │ ├── 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 ├── Command │ └── AddUserCommandTest.php ├── Controller │ ├── Admin │ │ └── BlogControllerTest.php │ ├── BlogControllerTest.php │ ├── DefaultControllerTest.php │ └── UserControllerTest.php ├── 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 ├── log │ └── .gitkeep └── sessions │ └── .gitkeep ├── webpack.config.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /.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 | /public/build/fonts/glyphicons-* 2 | /public/build/images/glyphicons-* 3 | 4 | ###> symfony/framework-bundle ### 5 | /.env.local 6 | /.env.*.local 7 | /public/bundles/ 8 | /var/ 9 | /vendor/ 10 | ###< symfony/framework-bundle ### 11 | 12 | ###> symfony/phpunit-bridge ### 13 | .phpunit 14 | /phpunit.xml 15 | ###< symfony/phpunit-bridge ### 16 | 17 | ###> symfony/web-server-bundle ### 18 | /.web-server-pid 19 | ###< symfony/web-server-bundle ### 20 | 21 | ###> friendsofphp/php-cs-fixer ### 22 | /.php_cs 23 | /.php_cs.cache 24 | ###< friendsofphp/php-cs-fixer ### 25 | 26 | ###> symfony/webpack-encore-bundle ### 27 | /node_modules/ 28 | npm-debug.log 29 | yarn-error.log 30 | ###< symfony/webpack-encore-bundle ### 31 | 32 | ##php strom 33 | /.idea/ 34 | -------------------------------------------------------------------------------- /.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 | ; 22 | 23 | return PhpCsFixer\Config::create() 24 | ->setRiskyAllowed(true) 25 | ->setRules([ 26 | '@Symfony' => true, 27 | '@Symfony:risky' => true, 28 | 'array_syntax' => ['syntax' => 'short'], 29 | 'header_comment' => ['header' => $fileHeaderComment, 'separate' => 'both'], 30 | 'linebreak_after_opening_tag' => true, 31 | 'mb_str_functions' => true, 32 | 'no_php4_constructor' => true, 33 | 'no_superfluous_phpdoc_tags' => true, 34 | 'no_unreachable_default_argument_value' => true, 35 | 'no_useless_else' => true, 36 | 'no_useless_return' => true, 37 | 'ordered_imports' => true, 38 | 'php_unit_strict' => true, 39 | 'phpdoc_order' => true, 40 | 'semicolon_after_instruction' => true, 41 | 'strict_comparison' => true, 42 | 'strict_param' => true, 43 | ]) 44 | ->setFinder($finder) 45 | ->setCacheFile(__DIR__.'/var/.php_cs.cache') 46 | ; 47 | -------------------------------------------------------------------------------- /.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_PHPUNIT_DIR=./bin/.phpunit 13 | - SYMFONY_DEPRECATIONS_HELPER=29 14 | - ACTION="install" 15 | 16 | matrix: 17 | fast_finish: true 18 | include: 19 | - php: 7.1.18 20 | - php: 7.2 21 | - php: 7.3 22 | # 'php: nightly' is PHP 8.0 23 | - php: 7.4snapshot 24 | - php: 7.3 25 | env: SYMFONY="4.4.*" 26 | ACTION="update" 27 | - php: 7.3 28 | env: SYMFONY="5.0.*" 29 | ACTION="update" 30 | allow_failures: 31 | - php: 7.4snapshot 32 | - php: 7.3 33 | env: SYMFONY="4.4.*" 34 | ACTION="update" 35 | - php: 7.3 36 | env: SYMFONY="5.0.*" 37 | ACTION="update" 38 | 39 | before_install: 40 | - '[[ "$TRAVIS_PHP_VERSION" == "7.4snapshot" ]] || phpenv config-rm xdebug.ini' 41 | - composer self-update 42 | # Set memory to max (memory fail) 43 | - '[[ "$ACTION" == "install" ]] || echo "memory_limit=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini' 44 | # Set stability to dev to allow 4.4dev and 5.0dev 45 | - '[[ "$ACTION" == "install" ]] || composer config minimum-stability dev' 46 | # Change version of symfony when need 47 | - '[[ "$ACTION" == "install" ]] || composer config extra.symfony.require $SYMFONY' 48 | 49 | install: 50 | - php -r "echo ini_get('memory_limit').PHP_EOL;" 51 | # install or update 52 | - composer $ACTION 53 | - ./bin/phpunit install 54 | 55 | script: 56 | - ./bin/phpunit 57 | # this checks that the source code follows the Symfony Code Syntax rules 58 | - '[[ "$TRAVIS_PHP_VERSION" == "7.4snapshot" ]] || ./vendor/bin/php-cs-fixer fix --diff --dry-run -v' 59 | # this checks that the YAML config files contain no syntax errors 60 | - ./bin/console lint:yaml config --parse-tags 61 | # this checks that the Twig template files contain no syntax errors 62 | - ./bin/console lint:twig templates --env=prod 63 | # this checks that the XLIFF translations contain no syntax errors 64 | - ./bin/console lint:xliff translations 65 | # this checks that the application doesn't use dependencies with known security vulnerabilities 66 | - ./bin/console security:check 67 | # this checks that Doctrine's mapping configurations are valid 68 | - ./bin/console doctrine:schema:validate --skip-sync -vvv --no-interaction 69 | # Fail CI if the repo is in a dirty state after building assets (only for current release ie install) 70 | - if [[ "$ACTION" == "install" ]]; then yarn install && yarn encore production && git add --all && git diff --staged --exit-code; fi 71 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM composer:1.9 as build 2 | WORKDIR /app/ 3 | COPY composer.json composer.lock /app/ 4 | RUN composer global require hirak/prestissimo && \ 5 | composer install --no-dev --no-scripts --no-autoloader \ 6 | && composer dump-autoload --optimize 7 | 8 | FROM php:7.3-apache-stretch 9 | RUN apt-get update && apt-get install -y \ 10 | acl \ 11 | && rm -rf /var/lib/apt/lists/* 12 | RUN docker-php-ext-install pdo 13 | WORKDIR /var/www/project 14 | 15 | ENV APP_ENV=prod 16 | ENV HTTPDUSER='www-data' 17 | 18 | EXPOSE 8080 19 | 20 | COPY docker/000-default.conf /etc/apache2/sites-available/ 21 | COPY --from=build /app/vendor /var/www/project/vendor 22 | COPY . /var/www/project/ 23 | 24 | RUN setfacl -dR -m u:"$HTTPDUSER":rwX -m u:$(whoami):rwX var && \ 25 | setfacl -R -m u:"$HTTPDUSER":rwX -m u:$(whoami):rwX var && \ 26 | echo "Listen 8080" >> /etc/apache2/ports.conf && \ 27 | mkdir -p /var/www/project/var/log/ && \ 28 | mkdir -p /var/www/project/var/cache/ && \ 29 | usermod -u 1000 www-data &&\ 30 | chown -R www-data:www-data /var/www/ && \ 31 | a2enmod rewrite 32 | USER www-data 33 | 34 | RUN php bin/console cache:clear --no-warmup && \ 35 | php bin/console cache:warmup 36 | 37 | CMD ["apache2-foreground"] 38 | -------------------------------------------------------------------------------- /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 | The "Symfony Demo Application" is a reference application created to show how 5 | to develop applications following the [Symfony Best Practices](https://symfony.com/doc/current/best_practices/index.html). Original [Read me](https://github.com/symfony/demo). This repo is part of [Symfony on Google Cloud Run](https://geshan.com.np/blog/2019/11/how-to-run-symfony-on-google-cloud-run-with-the-demo-app-step-by-step-guide/) blog post. 6 | 7 | ## Run Locally with docker and docker-compose 8 | 9 | Run the following command: 10 | 11 | ```bash 12 | docker-compose up 13 | ``` 14 | 15 | Then hit `http://localhost:8080` on your browser. 16 | 17 | ## Run on Google Cloud Run 18 | 19 | [![Run on Google Cloud](https://storage.googleapis.com/cloudrun/button.svg)](https://console.cloud.google.com/cloudshell/editor?shellonly=true&cloudshell_image=gcr.io/cloudrun/button&cloudshell_git_repo=https://github.com/geshan/symfony-demo-google-cloud-run.git) 20 | 21 | ## Gotcha 22 | 23 | As it uses a `sqlite` which is a local file, all changes will be lost as the file is not saved to a bucket. 24 | 25 | -------------------------------------------------------------------------------- /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() && !passwordEl.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 | -------------------------------------------------------------------------------- /data/database.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/data/database.sqlite -------------------------------------------------------------------------------- /data/database_test.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/data/database_test.sqlite -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: 5 | context: ./ 6 | volumes: 7 | - .:/var/www/project 8 | ports: 9 | - "8080:8080" 10 | environment: 11 | - APP_ENV=dev 12 | -------------------------------------------------------------------------------- /docker/000-default.conf: -------------------------------------------------------------------------------- 1 | 2 | #ServerName domain.tld 3 | #ServerAlias www.domain.tld 4 | 5 | DocumentRoot /var/www/project/public 6 | 7 | AllowOverride None 8 | Order Allow,Deny 9 | Allow from All 10 | 11 | 12 | Options -MultiViews 13 | RewriteEngine On 14 | RewriteCond %{REQUEST_FILENAME} !-f 15 | RewriteRule ^(.*)$ index.php [QSA,L] 16 | 17 | 18 | 19 | # uncomment the following lines if you install assets as symlinks 20 | # or run into problems when compiling LESS/Sass/CoffeeScript assets 21 | # 22 | # Options FollowSymlinks 23 | # 24 | 25 | # optionally disable the RewriteEngine for the asset directories 26 | # which will allow apache to simply reply with a 404 when files are 27 | # not found instead of passing the request into the full symfony stack 28 | 29 | 30 | RewriteEngine Off 31 | 32 | 33 | #ErrorLog /var/log/apache2/project_error.log 34 | #CustomLog /var/log/apache2/project_access.log combined 35 | PassEnv APP_ENV 36 | 37 | -------------------------------------------------------------------------------- /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": "^10.4.1", 12 | "imports-loader": "^0.8.0", 13 | "jquery": "^3.5.0", 14 | "lato-font": "^3.0.0", 15 | "node-sass": "^7.0.0", 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/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/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-/3gf9IHx1zzgGuRgdnnW/2AJBU896jAJn1dDLJLb3vBkLQkE8WvI3ycZt0l0GECV", 46 | "/build/app.css": "sha384-NGTdnyfcvVpvCvvEryRps/xU6Mb67YeTf1pvFrUkd80uXKIuxzYmcXzvzttKu4d0", 47 | "/build/login.js": "sha384-tCP53hf/3uu7W5h5syxKODJjjos7AyGM/lENitUwFKSXMxQsWP+50V0mN6bcImAo", 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/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/fa-brands-400.14c590d1.eot -------------------------------------------------------------------------------- /public/build/fonts/fa-brands-400.3e1b2a65.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/fa-brands-400.3e1b2a65.woff2 -------------------------------------------------------------------------------- /public/build/fonts/fa-brands-400.5e8aa9ea.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/fa-brands-400.5e8aa9ea.ttf -------------------------------------------------------------------------------- /public/build/fonts/fa-brands-400.df02c782.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/fa-brands-400.df02c782.woff -------------------------------------------------------------------------------- /public/build/fonts/fa-regular-400.285a9d2a.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/fa-regular-400.285a9d2a.ttf -------------------------------------------------------------------------------- /public/build/fonts/fa-regular-400.5623624d.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/fa-regular-400.5623624d.woff -------------------------------------------------------------------------------- /public/build/fonts/fa-regular-400.aa66d0e0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/fa-regular-400.aa66d0e0.eot -------------------------------------------------------------------------------- /public/build/fonts/fa-regular-400.ac21cac3.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/fa-regular-400.ac21cac3.woff2 -------------------------------------------------------------------------------- /public/build/fonts/fa-solid-900.3ded831d.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/fa-solid-900.3ded831d.woff -------------------------------------------------------------------------------- /public/build/fonts/fa-solid-900.42e1fbd2.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/fa-solid-900.42e1fbd2.eot -------------------------------------------------------------------------------- /public/build/fonts/fa-solid-900.896e20e2.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/fa-solid-900.896e20e2.ttf -------------------------------------------------------------------------------- /public/build/fonts/fa-solid-900.d6d8d5da.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/fa-solid-900.d6d8d5da.woff2 -------------------------------------------------------------------------------- /public/build/fonts/lato-bold-italic.0b6bb672.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/lato-bold-italic.0b6bb672.woff2 -------------------------------------------------------------------------------- /public/build/fonts/lato-bold-italic.9c7e4e9e.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/lato-bold-italic.9c7e4e9e.woff -------------------------------------------------------------------------------- /public/build/fonts/lato-bold.cccb8974.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/lato-bold.cccb8974.woff2 -------------------------------------------------------------------------------- /public/build/fonts/lato-bold.d878b6c2.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/lato-bold.d878b6c2.woff -------------------------------------------------------------------------------- /public/build/fonts/lato-normal-italic.4eb103b4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/lato-normal-italic.4eb103b4.woff2 -------------------------------------------------------------------------------- /public/build/fonts/lato-normal-italic.f28f2d64.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/lato-normal-italic.f28f2d64.woff -------------------------------------------------------------------------------- /public/build/fonts/lato-normal.27bd77b9.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/public/build/fonts/lato-normal.27bd77b9.woff -------------------------------------------------------------------------------- /public/build/fonts/lato-normal.bd03a2cc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/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()||i.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/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/Comment.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 | use Symfony\Component\Validator\Constraints as Assert; 16 | 17 | /** 18 | * @ORM\Entity 19 | * @ORM\Table(name="symfony_demo_comment") 20 | * 21 | * Defines the properties of the Comment entity to represent the blog comments. 22 | * See https://symfony.com/doc/current/book/doctrine.html#creating-an-entity-class 23 | * 24 | * Tip: if you have an existing database, you can generate these entity class automatically. 25 | * See https://symfony.com/doc/current/cookbook/doctrine/reverse_engineering.html 26 | * 27 | * @author Ryan Weaver 28 | * @author Javier Eguiluz 29 | */ 30 | class Comment 31 | { 32 | /** 33 | * @var int 34 | * 35 | * @ORM\Id 36 | * @ORM\GeneratedValue 37 | * @ORM\Column(type="integer") 38 | */ 39 | private $id; 40 | 41 | /** 42 | * @var Post 43 | * 44 | * @ORM\ManyToOne(targetEntity="Post", inversedBy="comments") 45 | * @ORM\JoinColumn(nullable=false) 46 | */ 47 | private $post; 48 | 49 | /** 50 | * @var string 51 | * 52 | * @ORM\Column(type="text") 53 | * @Assert\NotBlank(message="comment.blank") 54 | * @Assert\Length( 55 | * min=5, 56 | * minMessage="comment.too_short", 57 | * max=10000, 58 | * maxMessage="comment.too_long" 59 | * ) 60 | */ 61 | private $content; 62 | 63 | /** 64 | * @var \DateTime 65 | * 66 | * @ORM\Column(type="datetime") 67 | */ 68 | private $publishedAt; 69 | 70 | /** 71 | * @var User 72 | * 73 | * @ORM\ManyToOne(targetEntity="App\Entity\User") 74 | * @ORM\JoinColumn(nullable=false) 75 | */ 76 | private $author; 77 | 78 | public function __construct() 79 | { 80 | $this->publishedAt = new \DateTime(); 81 | } 82 | 83 | /** 84 | * @Assert\IsTrue(message="comment.is_spam") 85 | */ 86 | public function isLegitComment(): bool 87 | { 88 | $containsInvalidCharacters = false !== mb_strpos($this->content, '@'); 89 | 90 | return !$containsInvalidCharacters; 91 | } 92 | 93 | public function getId(): ?int 94 | { 95 | return $this->id; 96 | } 97 | 98 | public function getContent(): ?string 99 | { 100 | return $this->content; 101 | } 102 | 103 | public function setContent(string $content): void 104 | { 105 | $this->content = $content; 106 | } 107 | 108 | public function getPublishedAt(): \DateTime 109 | { 110 | return $this->publishedAt; 111 | } 112 | 113 | public function setPublishedAt(\DateTime $publishedAt): void 114 | { 115 | $this->publishedAt = $publishedAt; 116 | } 117 | 118 | public function getAuthor(): ?User 119 | { 120 | return $this->author; 121 | } 122 | 123 | public function setAuthor(User $author): void 124 | { 125 | $this->author = $author; 126 | } 127 | 128 | public function getPost(): ?Post 129 | { 130 | return $this->post; 131 | } 132 | 133 | public function setPost(Post $post): void 134 | { 135 | $this->post = $post; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /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/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/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/Controller/BlogControllerTest.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\Controller; 13 | 14 | use App\Entity\Post; 15 | use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; 16 | 17 | /** 18 | * Functional test for the controllers defined inside BlogController. 19 | * 20 | * See https://symfony.com/doc/current/book/testing.html#functional-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 BlogControllerTest extends WebTestCase 28 | { 29 | public function testIndex() 30 | { 31 | $client = static::createClient(); 32 | $crawler = $client->request('GET', '/en/blog/'); 33 | 34 | $this->assertCount( 35 | Post::NUM_ITEMS, 36 | $crawler->filter('article.post'), 37 | 'The homepage displays the right number of posts.' 38 | ); 39 | } 40 | 41 | public function testRss() 42 | { 43 | $client = static::createClient(); 44 | $crawler = $client->request('GET', '/en/blog/rss.xml'); 45 | 46 | $this->assertSame( 47 | 'text/xml; charset=UTF-8', 48 | $client->getResponse()->headers->get('Content-Type') 49 | ); 50 | 51 | $this->assertCount( 52 | Post::NUM_ITEMS, 53 | $crawler->filter('item'), 54 | 'The xml file displays the right number of posts.' 55 | ); 56 | } 57 | 58 | /** 59 | * This test changes the database contents by creating a new comment. However, 60 | * thanks to the DAMADoctrineTestBundle and its PHPUnit listener, all changes 61 | * to the database are rolled back when this test completes. This means that 62 | * all the application tests begin with the same database contents. 63 | */ 64 | public function testNewComment() 65 | { 66 | $client = static::createClient([], [ 67 | 'PHP_AUTH_USER' => 'john_user', 68 | 'PHP_AUTH_PW' => 'kitten', 69 | ]); 70 | $client->followRedirects(); 71 | 72 | // Find first blog post 73 | $crawler = $client->request('GET', '/en/blog/'); 74 | $postLink = $crawler->filter('article.post > h2 a')->link(); 75 | 76 | $crawler = $client->click($postLink); 77 | 78 | $form = $crawler->selectButton('Publish comment')->form([ 79 | 'comment[content]' => 'Hi, Symfony!', 80 | ]); 81 | $crawler = $client->submit($form); 82 | 83 | $newComment = $crawler->filter('.post-comment')->first()->filter('div > p')->text(); 84 | 85 | $this->assertSame('Hi, Symfony!', $newComment); 86 | } 87 | 88 | public function testAjaxSearch() 89 | { 90 | $client = static::createClient(); 91 | $client->xmlHttpRequest('GET', '/en/blog/search', ['q' => 'lorem']); 92 | 93 | $results = json_decode($client->getResponse()->getContent(), true); 94 | 95 | $this->assertSame('application/json', $client->getResponse()->headers->get('Content-Type')); 96 | $this->assertCount(1, $results); 97 | $this->assertSame('Lorem ipsum dolor sit amet consectetur adipiscing elit', $results[0]['title']); 98 | $this->assertSame('Jane Doe', $results[0]['author']); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/Controller/DefaultControllerTest.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\Controller; 13 | 14 | use App\Entity\Post; 15 | use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; 16 | use Symfony\Component\HttpFoundation\Response; 17 | 18 | /** 19 | * Functional test that implements a "smoke test" of all the public and secure 20 | * URLs of the application. 21 | * See https://symfony.com/doc/current/best_practices/tests.html#functional-tests. 22 | * 23 | * Execute the application tests using this command (requires PHPUnit to be installed): 24 | * 25 | * $ cd your-symfony-project/ 26 | * $ ./vendor/bin/phpunit 27 | */ 28 | class DefaultControllerTest extends WebTestCase 29 | { 30 | /** 31 | * PHPUnit's data providers allow to execute the same tests repeated times 32 | * using a different set of data each time. 33 | * See https://symfony.com/doc/current/cookbook/form/unit_testing.html#testing-against-different-sets-of-data. 34 | * 35 | * @dataProvider getPublicUrls 36 | */ 37 | public function testPublicUrls(string $url) 38 | { 39 | $client = static::createClient(); 40 | $client->request('GET', $url); 41 | 42 | $this->assertSame( 43 | Response::HTTP_OK, 44 | $client->getResponse()->getStatusCode(), 45 | sprintf('The %s public URL loads correctly.', $url) 46 | ); 47 | } 48 | 49 | /** 50 | * A good practice for tests is to not use the service container, to make 51 | * them more robust. However, in this example we must access to the container 52 | * to get the entity manager and make a database query. The reason is that 53 | * blog post fixtures are randomly generated and there's no guarantee that 54 | * a given blog post slug will be available. 55 | */ 56 | public function testPublicBlogPost() 57 | { 58 | $client = static::createClient(); 59 | // the service container is always available via the test client 60 | $blogPost = $client->getContainer()->get('doctrine')->getRepository(Post::class)->find(1); 61 | $client->request('GET', sprintf('/en/blog/posts/%s', $blogPost->getSlug())); 62 | 63 | $this->assertSame(Response::HTTP_OK, $client->getResponse()->getStatusCode()); 64 | } 65 | 66 | /** 67 | * The application contains a lot of secure URLs which shouldn't be 68 | * publicly accessible. This tests ensures that whenever a user tries to 69 | * access one of those pages, a redirection to the login form is performed. 70 | * 71 | * @dataProvider getSecureUrls 72 | */ 73 | public function testSecureUrls(string $url) 74 | { 75 | $client = static::createClient(); 76 | $client->request('GET', $url); 77 | 78 | $response = $client->getResponse(); 79 | $this->assertSame(Response::HTTP_FOUND, $response->getStatusCode()); 80 | $this->assertSame( 81 | 'http://localhost/en/login', 82 | $response->getTargetUrl(), 83 | sprintf('The %s secure URL redirects to the login form.', $url) 84 | ); 85 | } 86 | 87 | public function getPublicUrls() 88 | { 89 | yield ['/']; 90 | yield ['/en/blog/']; 91 | yield ['/en/login']; 92 | } 93 | 94 | public function getSecureUrls() 95 | { 96 | yield ['/en/admin/post/']; 97 | yield ['/en/admin/post/new']; 98 | yield ['/en/admin/post/1']; 99 | yield ['/en/admin/post/1/edit']; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/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/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/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/var/log/.gitkeep -------------------------------------------------------------------------------- /var/sessions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geshan/symfony-demo-google-cloud-run/e1fbea609aa72f79ea57fcaaad86e8764e1f35ad/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 | --------------------------------------------------------------------------------