├── .env ├── .env.test ├── .gitignore ├── .php_cs ├── .travis.yml ├── README.md ├── assets ├── js │ ├── app.js │ ├── button-click-back.js │ ├── class │ │ ├── BellNotification.js │ │ └── MercureSubscribe.js │ ├── delete-confirmation.js │ ├── delete-notification.js │ ├── mercure-subscribe.js │ └── tags.js └── scss │ ├── _base.scss │ ├── _spacing.scss │ ├── _variables.scss │ ├── admin │ ├── admin.scss │ └── design │ │ ├── _article.scss │ │ ├── _backoffice.scss │ │ ├── _dashboard.scss │ │ ├── _list.scss │ │ └── _sidebar.scss │ ├── app.scss │ └── design │ ├── _article.scss │ ├── _bell.scss │ ├── _flash-notification.scss │ ├── _homepage.scss │ ├── _login.scss │ ├── _navbar.scss │ └── _profile.scss ├── bin ├── console └── phpunit ├── composer.json ├── composer.lock ├── config ├── bootstrap.php ├── bundles.php ├── packages │ ├── assets.yaml │ ├── dev │ │ ├── monolog.yaml │ │ ├── nelmio_alice.yaml │ │ ├── routing.yaml │ │ ├── security_checker.yaml │ │ ├── swiftmailer.yaml │ │ └── web_profiler.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── eight_points_guzzle.yaml │ ├── framework.yaml │ ├── framework_extra.yaml │ ├── hautelook_alice.yaml │ ├── mercure.yaml │ ├── monolog.yaml │ ├── prod │ │ ├── doctrine.yaml │ │ └── monolog.yaml │ ├── routing.yaml │ ├── security.yaml │ ├── security_checker.yaml │ ├── stof_doctrine_extensions.yaml │ ├── swiftmailer.yaml │ ├── test │ │ ├── framework.yaml │ │ ├── nelmio_alice.yaml │ │ ├── swiftmailer.yaml │ │ └── web_profiler.yaml │ ├── translation.yaml │ ├── twig.yaml │ ├── twig_extensions.yaml │ └── webpack_encore.yaml ├── routes.yaml ├── routes │ ├── annotations.yaml │ └── dev │ │ ├── twig.yaml │ │ └── web_profiler.yaml ├── services.yaml ├── services_test.yaml └── services_travis.yaml ├── docker-compose.yml ├── docker ├── nginx │ └── nginx.conf └── php │ ├── Dockerfile │ └── php.ini ├── fixtures ├── article.yaml ├── comment.yaml ├── notification_type.yaml ├── tag.yaml └── user.yaml ├── package.json ├── phpunit.xml.dist ├── public ├── favicon.ico ├── index.php └── robots.txt ├── src ├── Controller │ ├── Admin │ │ ├── AdminController.php │ │ └── ArticleController.php │ ├── BlogController.php │ ├── NotificationController.php │ ├── SecurityController.php │ └── UserSettingsController.php ├── Entity │ ├── Article.php │ ├── Comment.php │ ├── Image.php │ ├── Notification.php │ ├── NotificationType.php │ ├── Tag.php │ ├── User.php │ └── UserNotification.php ├── EventSubscriber │ ├── PasswordTokenReset.php │ ├── PreferredLocaleSubscriber.php │ ├── SetMercureCookieSubscriber.php │ └── UserActionSubscriber.php ├── Events.php ├── Form │ ├── ArticleType.php │ ├── CommentType.php │ ├── DataTransformer │ │ └── TagsTransformer.php │ ├── ImageType.php │ ├── LoginType.php │ ├── PasswordResetNewType.php │ ├── PasswordResetRequestType.php │ ├── RegistrationType.php │ └── Type │ │ └── TagsType.php ├── Kernel.php ├── Migrations │ ├── Version20171224150255.php │ ├── Version20180209225711.php │ ├── Version20180314174711.php │ ├── Version20180402143231.php │ ├── Version20180423195809.php │ ├── Version20180513194510.php │ ├── Version20180607202731.php │ ├── Version20180607203541.php │ ├── Version20180611080116.php │ ├── Version20181224152453.php │ ├── Version20190913132213.php │ └── Version20190929114309.php ├── Repository │ ├── ArticleRepository.php │ ├── CommentRepository.php │ ├── ImageRepository.php │ ├── NotificationRepository.php │ ├── NotificationTypeRepository.php │ ├── TagRepository.php │ ├── UserNotificationRepository.php │ └── UserRepository.php ├── Security │ ├── LoginAuthenticator.php │ └── LoginGithubAuthenticator.php ├── Services │ ├── Article │ │ └── Manager │ │ │ └── ArticleManager.php │ ├── Faker │ │ └── Provider │ │ │ └── EncodePasswordProvider.php │ ├── FlashMessage.php │ ├── Mailer.php │ ├── MercureCookieGenerator.php │ ├── Notification │ │ └── Factory │ │ │ └── NotificationFactory.php │ ├── Notifier.php │ ├── Paginator.php │ ├── TokenPassword.php │ ├── Uploader.php │ ├── User │ │ └── Manager │ │ │ └── UserManager.php │ ├── UserActionLogger.php │ └── UserNotification │ │ └── Factory │ │ └── UserNotificationFactory.php └── Twig │ └── NotificationExtension.php ├── symfony.lock ├── templates ├── backoffice │ ├── article │ │ ├── add.html.twig │ │ ├── edit.html.twig │ │ └── list.html.twig │ ├── dashboard │ │ └── dashboard.html.twig │ ├── layout-backoffice.html.twig │ └── sidebar │ │ └── sidebar.html.twig ├── base.html.twig ├── blog │ ├── article │ │ ├── _comment_form.html.twig │ │ └── show.html.twig │ ├── home │ │ └── index.html.twig │ ├── layout-blog.html.twig │ ├── security │ │ ├── login │ │ │ ├── _login_form.html.twig │ │ │ └── login.html.twig │ │ ├── password │ │ │ ├── _password_reset_new_form.html.twig │ │ │ ├── _password_reset_request_form.html.twig │ │ │ ├── password_reset_new.html.twig │ │ │ └── password_reset_request.html.twig │ │ └── registration │ │ │ ├── _registration_form.html.twig │ │ │ └── registration.html.twig │ └── user │ │ └── profile │ │ ├── _sidebar.html.twig │ │ └── show.html.twig ├── email │ └── password_request │ │ ├── _password_reset_email_en.html.twig │ │ └── _password_reset_email_fr.html.twig ├── flashMessage │ └── flashMessage.html.twig ├── navbar │ ├── bell-notification.html.twig │ └── navbar.html.twig ├── notification-translations │ └── message.html.twig └── paginator │ └── paginator.html.twig ├── tests ├── BaseTestCase.php ├── Functional │ ├── ArticleActionTest.php │ ├── CommentTest.php │ ├── LoginTest.php │ ├── RouteTest.php │ └── SigninTest.php ├── Unit │ ├── Form │ │ └── DataTransformer │ │ │ └── TagsTransformerTest.php │ └── Services │ │ ├── Article │ │ └── Manager │ │ │ └── ArticleManagerTest.php │ │ ├── PaginatorTest.php │ │ ├── TokenPasswordTest.php │ │ ├── UploaderTest.php │ │ └── User │ │ └── Manager │ │ └── UserManagerTest.php └── bootstrap.php ├── translations ├── emails.en.yaml ├── emails.fr.yaml ├── messages.en.yaml ├── messages.fr.yaml ├── validators.en.yaml └── validators.fr.yaml ├── webpack.config.js └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | # In all environments, the following files are loaded if they exist, 2 | # the later taking precedence over the former: 3 | # 4 | # * .env contains default values for the environment variables needed by the app 5 | # * .env.local uncommitted file with local overrides 6 | # * .env.$APP_ENV committed environment-specific defaults 7 | # * .env.$APP_ENV.local uncommitted environment-specific overrides 8 | # 9 | # Real environment variables win over .env files. 10 | # 11 | # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. 12 | # 13 | # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). 14 | # https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration 15 | 16 | ###> symfony/framework-bundle ### 17 | APP_ENV=dev 18 | APP_SECRET=c5714b7b6af19e4c642f95f33da4bf1e 19 | # 127.0.0.1,127.0.0.2 20 | # localhost,example.com 21 | ###< symfony/framework-bundle ### 22 | 23 | ###> githubApp ### 24 | github_client_id=your_client_id 25 | github_secret_id=your_secret_id 26 | ###< githubApp ### 27 | 28 | ###> symfony/swiftmailer-bundle ### 29 | # For Gmail as a transport, use: "gmail://username:password@localhost" 30 | # For a generic SMTP server, use: "smtp://localhost:25?encryption=&auth_mode=" 31 | # Delivery is disabled by default via "null://localhost" 32 | MAILER_URL=null://localhost 33 | ###< symfony/swiftmailer-bundle ### 34 | 35 | ###> doctrine/doctrine-bundle ### 36 | # Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url 37 | # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" 38 | # Configure your db driver and server_version in config/packages/doctrine.yaml 39 | DATABASE_URL=mysql://root:secret@db:3306/symfony-blog 40 | ###< doctrine/doctrine-bundle ### 41 | 42 | ###> symfony/mercure-bundle ### 43 | # See https://symfony.com/doc/current/mercure.html#configuration 44 | MERCURE_PUBLISH_URL=http://mercure/hub 45 | MERCURE_JWT_SECRET=eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.2ZFWKrpjiywzQwLzoaSneOToQjkWuIDZ61Ij86Xg3wk 46 | ###< symfony/mercure-bundle ### 47 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | PANTHER_APP_ENV=panther 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | public/uploads 3 | ###> symfony/framework-bundle ### 4 | .env.local 5 | .env.local.php 6 | .env.*.local 7 | /public/bundles/ 8 | /var/ 9 | /vendor/ 10 | ###< symfony/framework-bundle ### 11 | ###> friendsofphp/php-cs-fixer ### 12 | .php_cs.cache 13 | ###< friendsofphp/php-cs-fixer ### 14 | ###> symfony/webpack-encore-bundle ### 15 | /node_modules/ 16 | /public/build/ 17 | npm-debug.log 18 | yarn-error.log 19 | ###< symfony/webpack-encore-bundle ### 20 | 21 | ###> symfony/phpunit-bridge ### 22 | .phpunit 23 | .phpunit.result.cache 24 | /phpunit.xml 25 | ###< symfony/phpunit-bridge ### 26 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | in(__DIR__) 5 | ->exclude('config') 6 | ->exclude('var') 7 | ->exclude('docker') 8 | ->exclude('public/build') 9 | ->exclude('Migrations'); 10 | 11 | return PhpCsFixer\Config::create() 12 | ->setRules([ 13 | '@Symfony' => true, 14 | 'array_syntax' => ['syntax' => 'short'], 15 | 'linebreak_after_opening_tag' => true, 16 | 'no_php4_constructor' => true, 17 | 'no_useless_else' => true, 18 | 'no_useless_return' => true, 19 | 'ordered_imports' => true, 20 | 'no_superfluous_elseif' => true 21 | ]) 22 | ->setFinder($finder); 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | dist: xenial 4 | 5 | services: 6 | - mysql 7 | 8 | addons: 9 | chrome: stable 10 | apt: 11 | sources: 12 | - mysql-5.7-trusty 13 | packages: 14 | - mysql-server 15 | - mysql-client 16 | 17 | cache: 18 | directories: 19 | - $HOME/.composer/cache/files 20 | - ./bin/.phpunit 21 | 22 | php: 23 | - '7.4' 24 | 25 | before_install: 26 | - chromium --headless --disable-gpu --remote-debugging-port=9222 http://localhost & 27 | 28 | install: 29 | - composer clearcache 30 | - composer install 31 | - ./bin/phpunit install 32 | - yarn install 33 | - yarn encore dev 34 | 35 | before_script: 36 | - sudo mysql -e "use mysql; update user set authentication_string=PASSWORD('secret_test') where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;" 37 | - sudo mysql_upgrade -u root -psecret_test 38 | - sudo service mysql restart 39 | - ./bin/console doctrine:database:create 40 | - ./bin/console doctrine:schema:create 41 | - ./bin/console hautelook:fixtures:load --no-interaction --no-debug 42 | 43 | script: 44 | # run tests 45 | - ./bin/phpunit 46 | # this checks that the YAML config files contain no syntax errors 47 | - ./bin/console lint:yaml config 48 | # this checks that the Twig template files contain no syntax errors 49 | - ./bin/console lint:twig templates 50 | # this checks that Doctrine's mapping configurations are valid 51 | - ./bin/console doctrine:schema:validate --skip-sync -vvv --no-interaction 52 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | import './delete-notification' 4 | import './button-click-back' 5 | import BellNotification from './class/BellNotification' 6 | 7 | new BellNotification($('#bell')) 8 | -------------------------------------------------------------------------------- /assets/js/button-click-back.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | $('#login-form').on('submit', function () { 4 | var translation = $('button:submit.button').data('trans'); 5 | $('button:submit.button').prop("disabled", true).html(translation); 6 | }); 7 | 8 | $('#registration-form').on('submit', function () { 9 | var translation = $('button:submit.button').data('trans'); 10 | $('button:submit.button').prop("disabled", true).html(translation); 11 | }); 12 | 13 | $('#password-reset-request-form').on('submit', function () { 14 | var translation = $('button:submit.button').data('trans'); 15 | $('button:submit.button').prop("disabled", true).html(translation); 16 | }); 17 | 18 | $('#password-reset-new-form').on('submit', function () { 19 | var translation = $('button:submit.button').data('trans'); 20 | $('button:submit.button').prop("disabled", true).html(translation); 21 | }); 22 | 23 | $('#comment-form').on('submit', function () { 24 | $('button:submit.button').prop("disabled", true); 25 | }); 26 | -------------------------------------------------------------------------------- /assets/js/class/BellNotification.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | export default class { 4 | constructor (bell) { 5 | this.bell = bell 6 | this.bellContainer = $('.bell-container') 7 | this.updateNotificationRoute = bell.data('update') 8 | this.active = false 9 | this.unreadCount = this.bell.data('notification-count') 10 | 11 | this.mounted() 12 | } 13 | 14 | mounted () { 15 | $(document).click(() => this.hide()) 16 | this.bellContainer.click(e => e.stopPropagation()) 17 | this.bell.click(e => e.stopPropagation()) 18 | 19 | this.bell.click(() => { 20 | this.toggleVisibility() 21 | this.update() 22 | }) 23 | } 24 | 25 | isActive () { 26 | return this.active 27 | } 28 | 29 | toggleVisibility () { 30 | if (this.isActive()) { 31 | return this.hide() 32 | } 33 | 34 | return this.show() 35 | } 36 | 37 | hide () { 38 | this.bellContainer.removeClass('bell-open') 39 | this.bell.removeClass('bell-active') 40 | this.active = false 41 | } 42 | 43 | show () { 44 | this.bellContainer.addClass('bell-open') 45 | this.bell.addClass('bell-active') 46 | this.active = true 47 | } 48 | 49 | update () { 50 | if (this.unreadCount <= 0) { 51 | return 52 | } 53 | 54 | $.ajax({ 55 | url: this.updateNotificationRoute, 56 | type: 'POST', 57 | done () { 58 | this.bell.data('notification-count', 0).addClass('notification-read') 59 | }, 60 | error () { 61 | alert(err.Message) 62 | } 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /assets/js/class/MercureSubscribe.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pifaace/symfony-blog/2c28217e824a8e048750ff87f598e883e225385a/assets/js/class/MercureSubscribe.js -------------------------------------------------------------------------------- /assets/js/delete-confirmation.js: -------------------------------------------------------------------------------- 1 | $('.delete-article').on('click', function() { 2 | var message = $('.delete-article').data('trans'); 3 | return confirm(message); 4 | }); 5 | 6 | $('.delete-image').on('click', function() { 7 | var message = $('.delete-image').data('trans'); 8 | if (confirm(message)) { 9 | $('.delete-img-confirm').prop('checked', true); 10 | $(this).parents("form").submit(); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /assets/js/delete-notification.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | $('.delete').on('click', function () { 4 | $('.notification').fadeOut("slow", function () { 5 | $('.flash-notification-container').remove(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /assets/js/mercure-subscribe.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | const url = new URL('http://localhost:3000/hub') 4 | url.searchParams.append('topic', 'http://symfony-blog.fr/new/article') 5 | 6 | const eventSource = new EventSource(url, {withCredentials: true}) 7 | 8 | // The callback will be called every time an update is published 9 | eventSource.onmessage = e => new newNotification(e) 10 | 11 | function newNotification(event) { 12 | const bell = $('#bell') 13 | const notificationCenter = $('#notification-center') 14 | const notification = JSON.parse(event.data) 15 | const url = new URL(notification.targetLink, window.location.href).href 16 | 17 | const notificationHtml = ` 18 | 19 |
20 | 21 | 22 | ${notification.createdBy.username} ${translations.article_created} 23 | 24 |
25 |
26 | ` 27 | 28 | 29 | if (notificationCenter.hasClass('bell-empty-content')) { 30 | notificationCenter.removeClass('bell-empty-content').addClass('bell-content') 31 | notificationCenter.parent().find('span').remove() 32 | } 33 | 34 | $('.bell-content').append(notificationHtml) 35 | 36 | animateBell() 37 | 38 | bell.removeClass('notification-read') 39 | let count = bell.attr('data-notification-count') 40 | count++ 41 | bell.attr('data-notification-count', count) 42 | } 43 | 44 | function animateBell() { 45 | $('.fa').addClass('ding').delay(1000).queue(function (next) { 46 | $(this).removeClass('ding') 47 | next() 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /assets/js/tags.js: -------------------------------------------------------------------------------- 1 | var tagInput = $('.tag-input'); 2 | var tagArray = []; 3 | var tagList = $('.hidden-tag-input').val(); 4 | 5 | tagInput.focus(function () { 6 | $(this).parent().addClass("input-focus"); 7 | }); 8 | 9 | tagInput.focusout(function () { 10 | $(this).parent().removeClass("input-focus"); 11 | }); 12 | 13 | if (tagList !== undefined) { 14 | tagArray = tagList.split(','); 15 | } 16 | 17 | tagInput.on('keyup', function () { 18 | var tag = tagInput.val(); 19 | if (/^[,,.]*$/.test(tag)) { 20 | $(this).val(""); 21 | } else if (tag.indexOf(',') !== -1) { 22 | var word = tag.replace(",", ""); 23 | tagArray.push(word); 24 | $('.hidden-tag-input').val(tagArray); 25 | tagInput.before("
" + word + "
"); 26 | 27 | $(this).val(""); 28 | } 29 | }); 30 | 31 | $('.tags').on('click', '.tag-remove', function () { 32 | var tag = $(this).parent(); 33 | var word = tag.text(); 34 | tagArray.splice($.inArray(word, tagArray), 1); 35 | 36 | $('.hidden-tag-input').val(tagArray); 37 | 38 | tag.remove(); 39 | }); 40 | -------------------------------------------------------------------------------- /assets/scss/_base.scss: -------------------------------------------------------------------------------- 1 | .flex-co { 2 | flex-direction: column; 3 | } 4 | 5 | .flex-1 { 6 | flex: 1; 7 | } 8 | 9 | .bg-grey { 10 | background-color: $grey-lighter; 11 | } 12 | 13 | .bg-light { 14 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 15 | } 16 | 17 | .dark-grey-color { 18 | color: $grey-dark; 19 | } 20 | 21 | .height-max { 22 | height: 100%; 23 | } 24 | 25 | .min-h-screen { 26 | min-height: 100vh; 27 | } 28 | 29 | .mt-30 { 30 | margin-top: 30px; 31 | } 32 | 33 | .bold { 34 | font-weight: bold; 35 | } 36 | 37 | .button:disabled { 38 | cursor: default; 39 | } 40 | 41 | .security-container { 42 | text-align: center; 43 | margin-bottom: 30px; 44 | 45 | .border-form { 46 | width: 350px; 47 | display: inline-block; 48 | background: $white; 49 | border: 0.5px solid #D9D9D9; 50 | border-radius: 7px; 51 | padding: 25px; 52 | } 53 | } 54 | 55 | .vertical-separator { 56 | border-right: 1px solid $grey-light; 57 | } 58 | 59 | .no-link-color { 60 | color: #363636 !important; 61 | } 62 | 63 | /** Override bulma class **/ 64 | hr { 65 | background-color: $grey-light; 66 | } 67 | 68 | .columns.is-gapless:not(:last-child) { 69 | height: 100%; 70 | margin-bottom: 0; 71 | } 72 | 73 | .navbar-brand { 74 | .navbar-item { 75 | font-size: 18px; 76 | } 77 | } 78 | 79 | a.navbar-item:hover { 80 | background-color: transparent; 81 | } 82 | 83 | .input-focus { 84 | border-color: $blue-light; 85 | outline: none; 86 | } 87 | 88 | .footer { 89 | background-color: $grey-footer; 90 | } 91 | 92 | /** Font awesome size icons extension **/ 93 | .fa-8x { 94 | font-size: 8em; 95 | } 96 | 97 | -------------------------------------------------------------------------------- /assets/scss/_spacing.scss: -------------------------------------------------------------------------------- 1 | $sizes: (5, 10, 15, 20, 25, 30, 35, 40); 2 | $positions: ( 3 | ('t', 'top'), 4 | ('b', 'bottom'), 5 | ('l', 'left'), 6 | ('r', 'right') 7 | ); 8 | $separator: '-'; 9 | $marginKey: 'm'; 10 | $paddingKey: 'p'; 11 | 12 | @each $size in $sizes { 13 | @each $position in $positions { 14 | $posKey: nth($position, 1); 15 | $posValue: nth($position, 2); 16 | 17 | .#{$marginKey}#{$posKey}#{$separator}#{$size} { 18 | margin#{$separator}#{$posValue}: $size + px; 19 | } 20 | 21 | .#{$paddingKey}#{$posKey}#{$separator}#{$size} { 22 | padding#{$separator}#{$posValue}: $size + px; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | // some colors 2 | $primary: #17a2b8; 3 | $grey-dark: #363636; 4 | $grey-light: #DCDCDC; 5 | $grey-lighter: #F3F3F3; 6 | $grey-footer: #DADADA; 7 | $red-light: #fff5f7; 8 | $red-strong: #cd0930; 9 | $blue-light: #80bdff; 10 | $secondary: #4a4a4a; 11 | 12 | $link: $primary; 13 | 14 | // menu-list variables 15 | $menu-list-link-and-hover: rgba(255, 255, 255, 0.23); 16 | 17 | // pagination variables 18 | $pagination-current-background-color: $primary; 19 | $pagination-current-border-color: $primary; 20 | 21 | // Errors messages variables 22 | $message-error-background-color: $red-light; 23 | $message-error-color: $red-strong; 24 | 25 | -------------------------------------------------------------------------------- /assets/scss/admin/admin.scss: -------------------------------------------------------------------------------- 1 | @import "design/dashboard"; 2 | @import "design/sidebar"; 3 | @import "design/backoffice"; 4 | @import "design/article"; 5 | @import "design/list"; 6 | -------------------------------------------------------------------------------- /assets/scss/admin/design/_article.scss: -------------------------------------------------------------------------------- 1 | .form-padding { 2 | padding: 35px; 3 | } 4 | 5 | .form-content { 6 | background: #FFFFFF; 7 | padding: 20px; 8 | } 9 | 10 | .form-part { 11 | display: inherit; 12 | background-color: #E3E3E3; 13 | font-size: 17px; 14 | margin: 15px 0 15px 0; 15 | padding: 9px 0 9px 9px; 16 | } 17 | 18 | .img-coverage-container { 19 | position: relative; 20 | display: inline-block; 21 | 22 | &:hover .img-coverage { 23 | opacity: 0.5; 24 | } 25 | 26 | &:hover .overlay { 27 | opacity: 1; 28 | } 29 | 30 | .img-coverage { 31 | opacity: 1; 32 | display: block; 33 | height: auto; 34 | transition: .3s ease; 35 | backface-visibility: hidden; 36 | -webkit-transition: all .3s; /* Safari */ 37 | } 38 | 39 | .overlay { 40 | transition: .5s ease; 41 | opacity: 0; 42 | position: absolute; 43 | top: 50%; 44 | left: 50%; 45 | transform: translate(-50%, -50%); 46 | -ms-transform: translate(-50%, -50%) 47 | } 48 | 49 | .delete-image { 50 | cursor: pointer; 51 | } 52 | 53 | } 54 | 55 | .tags { 56 | background-color: #fff; 57 | padding: 8px 16px 8px 16px; 58 | border: 1px solid rgba(0, 0, 0, 0.15); 59 | border-radius: 0.25rem; 60 | transition: border 0.15s ease-out; 61 | input { 62 | display: block; 63 | float: left; 64 | font-size: 1em; 65 | outline: none; 66 | } 67 | 68 | .tag { 69 | font-size: 14px; 70 | margin-bottom: 0; 71 | margin-top: 0; 72 | background-color: #55D3E7; 73 | } 74 | 75 | .tag-input { 76 | border: 0; 77 | padding: 0.25em 0.5em; 78 | } 79 | 80 | .tag-remove { 81 | display: inline-block; 82 | padding-left: 10px; 83 | 84 | &:hover { 85 | color: #E3E3E3; 86 | cursor: pointer; 87 | } 88 | } 89 | 90 | &.input-focus { 91 | border-color: #3273dc; 92 | } 93 | 94 | &:after { 95 | content: ""; 96 | clear: both; 97 | display: table; 98 | } 99 | 100 | &:last-child { 101 | margin-bottom: 0; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /assets/scss/admin/design/_backoffice.scss: -------------------------------------------------------------------------------- 1 | .main-panel { 2 | float: right; 3 | width: calc(100% - 280px); 4 | -webkit-transition: width 0.5s; 5 | transition: width 0.5s; 6 | 7 | @media screen and (max-width: 1024px){ 8 | width: calc(100% - 80px); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /assets/scss/admin/design/_dashboard.scss: -------------------------------------------------------------------------------- 1 | .box.stats-card { 2 | @extend .has-text-centered; 3 | @extend .is-inline-block; 4 | width: 325px; 5 | } 6 | -------------------------------------------------------------------------------- /assets/scss/admin/design/_list.scss: -------------------------------------------------------------------------------- 1 | .title-tab { 2 | margin-top: 15px; 3 | } 4 | 5 | .table-container { 6 | overflow-x: auto; 7 | } 8 | 9 | .table { 10 | width: 100%; 11 | margin-top: 20px; 12 | } 13 | -------------------------------------------------------------------------------- /assets/scss/admin/design/_sidebar.scss: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | position: fixed; 3 | top: 53px; 4 | width: 280px; 5 | height: 100%; 6 | background-color: $primary; 7 | padding-left: 0; 8 | padding-right: 0; 9 | -webkit-transition: width 0.5s; 10 | transition: width 0.5s; 11 | 12 | .menu-list a.is-active, 13 | .menu-list a.is-active:hover { 14 | background-color: $menu-list-link-and-hover; 15 | } 16 | 17 | .nav-link { 18 | color: $white; 19 | border-radius: 4px; 20 | text-align: center; 21 | margin: 5px 15px; 22 | 23 | &:hover { 24 | color: $white; 25 | background-color: rgba(255, 255, 255, 0.1); 26 | -webkit-transition: background-color 0.3s; /* Safari */ 27 | transition: background-color 0.3s; 28 | } 29 | } 30 | 31 | .sidebar-item-title { 32 | @media screen and (max-width: 1024px){ 33 | display: none; 34 | } 35 | } 36 | 37 | @media screen and (max-width: 1024px){ 38 | width: 80px; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /assets/scss/app.scss: -------------------------------------------------------------------------------- 1 | // Variables Bulma 2 | @import "~bulma/sass/utilities/initial-variables"; 3 | 4 | // Import overwride variables 5 | @import "variables"; 6 | 7 | // Import Bulma 8 | @import "~bulma/bulma"; 9 | 10 | // Import Font-awesome 11 | @import "~font-awesome/scss/font-awesome"; 12 | 13 | @import "base"; 14 | @import "spacing"; 15 | 16 | @import "admin/admin"; 17 | @import "design/homepage"; 18 | @import "design/login"; 19 | @import "design/flash-notification"; 20 | @import "design/navbar"; 21 | @import "design/article"; 22 | @import "design/profile"; 23 | -------------------------------------------------------------------------------- /assets/scss/design/_article.scss: -------------------------------------------------------------------------------- 1 | ul.article-infos li { 2 | display: inline-block; 3 | @extend .mr-10; 4 | } 5 | 6 | .comments-separator { 7 | height: 1px; 8 | } 9 | -------------------------------------------------------------------------------- /assets/scss/design/_bell.scss: -------------------------------------------------------------------------------- 1 | #bell { 2 | position: relative; 3 | cursor: pointer; 4 | 5 | &:hover { 6 | background-color: $navbar-item-hover-background-color; 7 | } 8 | 9 | &::after { 10 | position: absolute; 11 | content: attr(data-notification-count); 12 | top: 7px; 13 | right: 1px; 14 | border-radius: 60px; 15 | background: $primary; 16 | color: #fff; 17 | font-weight: 800; 18 | font-size: 11px; 19 | border: solid 2px #fff; 20 | padding: 0 5px; 21 | } 22 | 23 | &.notification-read::after { 24 | content: none; 25 | } 26 | 27 | .ding { 28 | animation: ring 1.5s ease; 29 | } 30 | } 31 | 32 | .bell-active { 33 | color: #17a2b8; 34 | background-color: #fafafa; 35 | } 36 | 37 | .bell-container { 38 | display: none; 39 | position: absolute; 40 | top: 52px; 41 | right: 157px; 42 | width: 380px; 43 | border-right: 1px solid #ABA9A9; 44 | border-bottom: 1px solid #ABA9A9; 45 | border-left: 1px solid #ABA9A9; 46 | box-shadow: 4px 8px 12px 0 rgba(0, 0, 0, 0.3); 47 | background: white; 48 | color: #666; 49 | border-radius: 5px; 50 | overflow: hidden; 51 | transition: all 0.3s ease-in-out; 52 | z-index: 2; 53 | 54 | .bell-header { 55 | background-color: $primary; 56 | color: white; 57 | text-align: center; 58 | padding: 10px 20px; 59 | height: 40px; 60 | font-size: 16px; 61 | transition: all 0.3s ease-in-out; 62 | font-weight: bold; 63 | } 64 | 65 | .bell-empty-content { 66 | display: flex; 67 | align-items: center; 68 | justify-content: center; 69 | min-height: calc(215px - 40px); 70 | } 71 | 72 | .bell-content { 73 | min-height: calc(215px - 40px); 74 | } 75 | 76 | .vertical-bar::before { 77 | content: ""; 78 | width: 3px; 79 | height: 100%; 80 | background-color: #ddd; 81 | position: absolute; 82 | left: 24px; 83 | z-index: -1; 84 | } 85 | 86 | .bell-notification-item { 87 | position: relative; 88 | transition: background-color .2s; 89 | 90 | &::after{ 91 | content: ""; 92 | position: absolute; 93 | left: 0; 94 | width: 0; 95 | top: 0; 96 | height: 100%; 97 | background-color: #1fa1b8; 98 | transition: all 0.2s; 99 | z-index: -2; 100 | } 101 | 102 | &:hover::after{ 103 | width: 27px; 104 | z-index: 0; 105 | } 106 | 107 | &:hover { 108 | background: #f1f1f1; 109 | } 110 | 111 | &:hover i { 112 | color: $primary; 113 | } 114 | } 115 | 116 | .bell-notification-icon { 117 | display: inline-block; 118 | vertical-align: middle; 119 | color: #dddddd; 120 | transition: all .2s; 121 | border-radius: 40px; 122 | position: relative; 123 | width: 20px; 124 | text-align: center; 125 | line-height: 20px !important; 126 | background-color: #ffffff; 127 | z-index: 2; 128 | } 129 | 130 | .bell-notification-content { 131 | display: inline-block; 132 | vertical-align: middle; 133 | } 134 | 135 | .bell-notification-time { 136 | font-size: 15px; 137 | } 138 | } 139 | 140 | .bell-open { 141 | display: block; 142 | } 143 | 144 | @keyframes ring { 145 | 0% { 146 | transform: rotate(10deg); 147 | } 148 | 12.5% { 149 | transform: rotate(-10deg); 150 | } 151 | 25% { 152 | transform: rotate(20deg); 153 | } 154 | 37.5% { 155 | transform: rotate(-25deg); 156 | } 157 | 50% { 158 | transform: rotate(20deg); 159 | } 160 | 62.5% { 161 | transform: rotate(-10deg); 162 | } 163 | 75% { 164 | transform: rotate(5deg); 165 | } 166 | 100% { 167 | transform: rotate(0deg); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /assets/scss/design/_flash-notification.scss: -------------------------------------------------------------------------------- 1 | .flash-notification-container { 2 | position: fixed; 3 | width: 380px; 4 | right: -380px; 5 | top: 76px; 6 | z-index: 999; 7 | -webkit-animation-name: slide-on; /* Safari 4.0 - 8.0 */ 8 | -webkit-animation-duration: 1.5s; /* Safari 4.0 - 8.0 */ 9 | animation-name: slide-on; 10 | animation-duration: 6s; 11 | animation-delay: 0.5s; 12 | } 13 | 14 | /* Safari 4.0 - 8.0 */ 15 | @-webkit-keyframes slide-on { 16 | 10% {transform: translateX(-107%)} 17 | 11%, 90% {transform: translateX(-107%)} 18 | 100% {transform: translateX(380px)} 19 | } 20 | 21 | -------------------------------------------------------------------------------- /assets/scss/design/_homepage.scss: -------------------------------------------------------------------------------- 1 | .card-space { 2 | margin: 30px 0 30px 0; 3 | } 4 | 5 | .pagination-container { 6 | margin: 20px 0 20px 0; 7 | } 8 | -------------------------------------------------------------------------------- /assets/scss/design/_login.scss: -------------------------------------------------------------------------------- 1 | .security-container { 2 | .login { 3 | .login-subtitle { 4 | margin-top: 15px; 5 | } 6 | 7 | .login-message { 8 | background-color: $message-error-background-color; 9 | color: $message-error-color; 10 | padding: 8px 0 8px 0; 11 | margin-bottom: 10px; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /assets/scss/design/_navbar.scss: -------------------------------------------------------------------------------- 1 | .navbar { 2 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 3 | 4 | .navbar-end { 5 | .navbar-action { 6 | padding: 0 4px 0 0; 7 | } 8 | 9 | @import "bell"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /assets/scss/design/_profile.scss: -------------------------------------------------------------------------------- 1 | .menu-list a:hover { 2 | background-color: $grey-light; 3 | } 4 | 5 | .profile-picture { 6 | border-radius: 130px; 7 | } 8 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(['--env', '-e'], null, true)) { 22 | putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); 23 | } 24 | 25 | if ($input->hasParameterOption('--no-debug', true)) { 26 | putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); 27 | } 28 | 29 | require dirname(__DIR__).'/config/bootstrap.php'; 30 | 31 | if ($_SERVER['APP_DEBUG']) { 32 | umask(0000); 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 | $_ENV += $env; 11 | } elseif (!class_exists(Dotenv::class)) { 12 | throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); 13 | } else { 14 | // load all the .env files 15 | (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env'); 16 | } 17 | 18 | $_SERVER += $_ENV; 19 | $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; 20 | $_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; 21 | $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; 22 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true], 6 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 7 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 8 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], 9 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 10 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 11 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 12 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 13 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true], 14 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 15 | Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true], 16 | EightPoints\Bundle\GuzzleBundle\EightPointsGuzzleBundle::class => ['all' => true], 17 | Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], 18 | Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle::class => ['dev' => true, 'test' => true, 'travis' => true], 19 | Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle::class => ['dev' => true, 'test' => true, 'travis' => true], 20 | Hautelook\AliceBundle\HautelookAliceBundle::class => ['dev' => true, 'test' => true, 'travis' => true], 21 | Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true], 22 | ]; 23 | -------------------------------------------------------------------------------- /config/packages/assets.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | assets: 3 | json_manifest_path: '%kernel.project_dir%/public/build/manifest.json' 4 | -------------------------------------------------------------------------------- /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", "!user_action"] 8 | user_action: 9 | type: stream 10 | path: "%kernel.logs_dir%/user_action.log" 11 | level: debug 12 | channels: ["user_action"] 13 | # uncomment to get logging in your browser 14 | # you may have to allow bigger header sizes in your Web server configuration 15 | #firephp: 16 | # type: firephp 17 | # level: info 18 | #chromephp: 19 | # type: chromephp 20 | # level: info 21 | console: 22 | type: console 23 | process_psr_3_messages: false 24 | channels: ["!event", "!doctrine", "!console"] 25 | -------------------------------------------------------------------------------- /config/packages/dev/nelmio_alice.yaml: -------------------------------------------------------------------------------- 1 | nelmio_alice: 2 | functions_blacklist: 3 | - 'current' 4 | - 'shuffle' 5 | - 'date' 6 | - 'time' 7 | - 'file' 8 | - 'md5' 9 | - 'sha1' 10 | -------------------------------------------------------------------------------- /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. 3 | # This allows you to run cache:warmup even if your 4 | # environment variables are not available yet. 5 | # You should not need to change this value. 6 | env(database_url): '' 7 | 8 | doctrine: 9 | dbal: 10 | # configure these for your database server 11 | driver: 'pdo_mysql' 12 | server_version: '5.7' 13 | charset: utf8mb4 14 | 15 | # With Symfony 3.3, remove the `resolve:` prefix 16 | url: '%env(resolve:DATABASE_URL)%' 17 | orm: 18 | auto_generate_proxy_classes: '%kernel.debug%' 19 | naming_strategy: doctrine.orm.naming_strategy.underscore 20 | auto_mapping: true 21 | mappings: 22 | App: 23 | is_bundle: false 24 | type: annotation 25 | dir: '%kernel.project_dir%/src/Entity' 26 | prefix: 'App\Entity' 27 | alias: App 28 | -------------------------------------------------------------------------------- /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/eight_points_guzzle.yaml: -------------------------------------------------------------------------------- 1 | eight_points_guzzle: 2 | clients: 3 | github_oauth: 4 | base_url: "https://github.com" 5 | options: 6 | headers: 7 | Accept: "application/json" 8 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: '%env(APP_SECRET)%' 3 | csrf_protection: ~ 4 | validation: { enable_annotations: true } 5 | #serializer: { enable_annotations: true } 6 | trusted_hosts: ~ 7 | session: 8 | # https://symfony.com/doc/current/reference/configuration/framework.html#handler-id 9 | handler_id: session.handler.native_file 10 | save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%' 11 | fragments: ~ 12 | http_method_override: true 13 | php_errors: 14 | log: true 15 | -------------------------------------------------------------------------------- /config/packages/framework_extra.yaml: -------------------------------------------------------------------------------- 1 | sensio_framework_extra: 2 | router: 3 | annotations: false 4 | -------------------------------------------------------------------------------- /config/packages/hautelook_alice.yaml: -------------------------------------------------------------------------------- 1 | hautelook_alice: 2 | fixtures_path: fixtures 3 | -------------------------------------------------------------------------------- /config/packages/mercure.yaml: -------------------------------------------------------------------------------- 1 | mercure: 2 | hubs: 3 | default: 4 | url: '%env(MERCURE_PUBLISH_URL)%' 5 | jwt: '%env(MERCURE_JWT_SECRET)%' 6 | -------------------------------------------------------------------------------- /config/packages/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | channels: ['user_action'] 3 | -------------------------------------------------------------------------------- /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_404s: 8 | # regex: exclude all 404 errors from the logs 9 | - ^/ 10 | channels: ["!user_action"] 11 | user_action: 12 | type: stream 13 | path: "%kernel.logs_dir%/user_action.log" 14 | level: debug 15 | channels: ["user_action"] 16 | nested: 17 | type: stream 18 | path: "%kernel.logs_dir%/%kernel.environment%.log" 19 | level: debug 20 | console: 21 | type: console 22 | process_psr_3_messages: false 23 | channels: ["!event", "!doctrine"] 24 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: ~ 4 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | encoders: 3 | App\Entity\User: 4 | algorithm: auto 5 | 6 | providers: 7 | database_users: 8 | entity: 9 | class: App:User 10 | property: username 11 | 12 | firewalls: 13 | dev: 14 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 15 | security: false 16 | 17 | main: 18 | anonymous: true 19 | pattern: ^/ 20 | guard: 21 | authenticator: 22 | - App\Security\LoginAuthenticator 23 | - App\Security\LoginGithubAuthenticator 24 | entry_point: App\Security\LoginAuthenticator 25 | 26 | logout: 27 | path: /logout 28 | target: / 29 | 30 | role_hierarchy: 31 | ROLE_ADMIN: ROLE_USER 32 | 33 | access_control: 34 | - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } 35 | - { path: ^/admin, roles: ROLE_ADMIN } 36 | - { path: ^/user, roles: ROLE_USER } 37 | -------------------------------------------------------------------------------- /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/stof_doctrine_extensions.yaml: -------------------------------------------------------------------------------- 1 | stof_doctrine_extensions: 2 | default_locale: fr_FR 3 | orm: 4 | default: 5 | sluggable: true 6 | -------------------------------------------------------------------------------- /config/packages/swiftmailer.yaml: -------------------------------------------------------------------------------- 1 | swiftmailer: 2 | url: '%env(MAILER_URL)%' 3 | spool: { type: 'memory' } 4 | -------------------------------------------------------------------------------- /config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_id: session.storage.mock_file 5 | -------------------------------------------------------------------------------- /config/packages/test/nelmio_alice.yaml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: ../dev/nelmio_alice.yaml } 3 | -------------------------------------------------------------------------------- /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 | paths: 5 | - '%kernel.project_dir%/translations/' 6 | fallbacks: 7 | - '%locale%' 8 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | paths: ['%kernel.project_dir%/templates'] 3 | debug: '%kernel.debug%' 4 | strict_variables: '%kernel.debug%' 5 | globals: 6 | image_article_coverage_directory: '%image_articles_coverages_directory%' 7 | image_article_coverage_display: '%image_articles_coverages_display%' 8 | notifications: '@App\Twig\NotificationExtension' 9 | -------------------------------------------------------------------------------- /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: ~ 10 | #Twig\Extensions\TextExtension: ~ 11 | -------------------------------------------------------------------------------- /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 | logout: 2 | path: /logout 3 | -------------------------------------------------------------------------------- /config/routes/annotations.yaml: -------------------------------------------------------------------------------- 1 | controllers: 2 | resource: ../../src/Controller/ 3 | type: annotation 4 | -------------------------------------------------------------------------------- /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 | image_articles_coverages_directory: '%kernel.project_dir%/public/uploads/articles/coverages/' 6 | image_articles_coverages_display: '/uploads/articles/coverages/' 7 | itemperpage: 5 8 | supportedLocales: 9 | - en 10 | - fr 11 | env(MERCURE_PUBLISH_URL): '' 12 | env(MERCURE_JWT_SECRET): '' 13 | mercure_secret_key: symfonyBlogJwtToken 14 | 15 | services: 16 | _defaults: 17 | autowire: true # Automatically injects dependencies in your services. 18 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 19 | public: false # Allows optimizing the container by removing unused services; this also means 20 | # fetching services directly from the container via $container->get() won't work. 21 | # The best practice is to be explicit about your dependencies anyway. 22 | bind: 23 | $userActionLogger: '@monolog.logger.user_action' 24 | $itemPerPage: '%itemperpage%' 25 | $targetDir: '%image_articles_coverages_directory%' 26 | $supportedLocales: '%supportedLocales%' 27 | $locale: '%locale%' 28 | 29 | # makes classes in src/ available to be used as services 30 | # this creates a service per class whose id is the fully-qualified class name 31 | App\: 32 | resource: '../src/*' 33 | exclude: '../src/{Entity,Migrations,Tests}' 34 | 35 | # controllers are imported separately to make sure services can be injected 36 | # as action arguments even if you don't extend any base controller class 37 | App\Controller\: 38 | resource: '../src/Controller' 39 | tags: ['controller.service_arguments'] 40 | 41 | twig.extension.text: 42 | class: Twig_Extensions_Extension_Text 43 | tags: 44 | - { name: twig.extension } 45 | 46 | App\Security\LoginGithubAuthenticator: 47 | arguments: ["@eight_points_guzzle.client.github_oauth"] 48 | 49 | App\EventSubscriber\UserActionSubscriber: 50 | tags: 51 | - { name: doctrine.event_subscriber, connection: default } 52 | 53 | App\EventSubscriber\PasswordTokenReset: 54 | tags: 55 | - { name: kernel.event_listener, event: token.reseted, method: resetToken } 56 | 57 | App\Services\Faker\Provider\EncodePasswordProvider: 58 | tags: 59 | - { name: nelmio_alice.faker.provider } 60 | 61 | App\Services\MercureCookieGenerator: 62 | arguments: 63 | - '%mercure_secret_key%' 64 | -------------------------------------------------------------------------------- /config/services_test.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | public: true 4 | 5 | # If you need to access services in a test, create an alias 6 | # and then fetch that alias from the container. As a convention, 7 | # aliases are prefixed with test. 8 | test.repository.article_repository: '@App\Repository\ArticleRepository' 9 | -------------------------------------------------------------------------------- /config/services_travis.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | public: true 4 | 5 | # If you need to access services in a test, create an alias 6 | # and then fetch that alias from the container. As a convention, 7 | # aliases are prefixed with test. 8 | test.repository.article_repository: '@App\Repository\ArticleRepository' 9 | test.repository.user_repository: '@App\Repository\UserRepository' 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | blog-server: 5 | build: ./docker/php 6 | image: blog-symfony 7 | depends_on: 8 | - db 9 | volumes: 10 | - ./:/application:cached 11 | - /application/var/sessions/ 12 | 13 | db: 14 | image: mysql:5.7 15 | ports: 16 | - '3306:3306' 17 | environment: 18 | MYSQL_ROOT_PASSWORD: secret 19 | MYSQL_DATABASE: symfony-blog 20 | volumes: 21 | - ./tmp/db:/var/lib/mysql 22 | 23 | db-test: 24 | image: mysql:5.7 25 | ports: 26 | - '3307:3306' 27 | environment: 28 | MYSQL_ROOT_PASSWORD: secret 29 | MYSQL_DATABASE: symfony-blog-test 30 | MYSQL_USER: gandalf 31 | MYSQL_PASSWORD: secret_test 32 | volumes: 33 | - ./tmp/test:/var/lib/mysql 34 | 35 | nginx: 36 | image: nginx 37 | ports: 38 | - '8000:80' 39 | volumes: 40 | - ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf 41 | - ./:/application:cached 42 | - ./var/log/nginx:/var/log/nginx 43 | depends_on: 44 | - blog-server 45 | 46 | mercure: 47 | image: dunglas/mercure 48 | ports: 49 | - '3000:80' 50 | environment: 51 | - JWT_KEY=symfonyBlogJwtToken 52 | - PUBLISH_ALLOWED_ORIGINS=* 53 | - CORS_ALLOWED_ORIGINS=http://symfony-blog.fr:8000 54 | - DEBUG=1 55 | -------------------------------------------------------------------------------- /docker/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | root /application/public; 3 | 4 | server_name symfony-blog.fr; 5 | 6 | location / { 7 | try_files $uri /index.php$is_args$args; 8 | } 9 | location ~ ^/index\.php(/|$) { 10 | fastcgi_pass blog-server:9000; 11 | fastcgi_split_path_info ^(.+\.php)(/.*)$; 12 | include fastcgi_params; 13 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 14 | fastcgi_param DOCUMENT_ROOT $realpath_root; 15 | } 16 | 17 | location ~ \.php$ { 18 | return 404; 19 | } 20 | 21 | error_log /var/log/nginx/symfony_error.log; 22 | access_log /var/log/nginx/symfony_access.log; 23 | } 24 | -------------------------------------------------------------------------------- /docker/php/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.4-fpm 2 | 3 | # Installing dependencies 4 | RUN apt-get update && apt-get install -y \ 5 | build-essential \ 6 | default-mysql-client \ 7 | openssl \ 8 | locales \ 9 | zip \ 10 | zlib1g-dev \ 11 | libicu-dev \ 12 | libzip-dev \ 13 | chromium 14 | 15 | #RUN curl -sS -o /tmp/icu.tgz -L http://download.icu-project.org/files/icu4c/62.1/icu4c-62_1-src.tgz \ 16 | # && tar -zxf /tmp/icu.tgz -C /tmp \ 17 | # && cd /tmp/icu/source \ 18 | # && ./configure --prefix=/usr/local \ 19 | # && make \ 20 | # && make install 21 | 22 | # Clear cache 23 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* 24 | 25 | # Installing extensions 26 | RUN docker-php-ext-install \ 27 | pdo_mysql \ 28 | zip \ 29 | intl \ 30 | opcache 31 | 32 | #RUN docker-php-ext-configure intl --with-icu-dir=/usr/local 33 | 34 | COPY php.ini /usr/local/etc/php/php.ini 35 | 36 | # Setting locales 37 | RUN echo fr_FR.UTF-8 UTF-8 > /etc/locale.gen && locale-gen 38 | 39 | # Installing composer 40 | RUN curl -sS https://getcomposer.org/installer | \ 41 | php -- --install-dir=/usr/bin/ --filename=composer 42 | 43 | # Changing Workdir 44 | WORKDIR /application 45 | 46 | ENV PANTHER_NO_SANDBOX 1 47 | 48 | RUN mkdir -p \ 49 | var/sessions \ 50 | && chown -R www-data var 51 | -------------------------------------------------------------------------------- /docker/php/php.ini: -------------------------------------------------------------------------------- 1 | session.auto_start = Off 2 | short_open_tag = Off 3 | date.timezone = Europe/Paris 4 | php_zip.dll 5 | 6 | # http://symfony.com/doc/current/performance.html 7 | opcache.max_accelerated_files = 20000 8 | realpath_cache_size = 4096K 9 | realpath_cache_ttl = 600 10 | -------------------------------------------------------------------------------- /fixtures/article.yaml: -------------------------------------------------------------------------------- 1 | App\Entity\Article: 2 | article_{1..10}: 3 | title: '' 4 | content: '' 5 | author: '@admin' 6 | tags: 'x @tag*' 7 | -------------------------------------------------------------------------------- /fixtures/comment.yaml: -------------------------------------------------------------------------------- 1 | App\Entity\Comment: 2 | comment_{1..10}: 3 | content: '' 4 | article: '@article_' 5 | user: '@johnDoe' 6 | -------------------------------------------------------------------------------- /fixtures/notification_type.yaml: -------------------------------------------------------------------------------- 1 | App\Entity\NotificationType: 2 | article_created_type: 3 | name: 'article_created' 4 | -------------------------------------------------------------------------------- /fixtures/tag.yaml: -------------------------------------------------------------------------------- 1 | App\Entity\Tag: 2 | tag_{0..20}: 3 | name: '' 4 | -------------------------------------------------------------------------------- /fixtures/user.yaml: -------------------------------------------------------------------------------- 1 | App\Entity\User: 2 | admin: 3 | username: 'admin' 4 | password: '' 5 | email: 'admin@gmail.com' 6 | role: ['ROLE_ADMIN'] 7 | johnDoe: 8 | username: 'johnDoe' 9 | password: '' 10 | email: 'john@gmail.com' 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony-blog", 3 | "version": "1.0.0", 4 | "description": "symfony-blog", 5 | "main": "app.js", 6 | "dependencies": { 7 | "bulma": "^0.7.1", 8 | "font-awesome": "^4.7.0", 9 | "jquery": "^3.3.1" 10 | }, 11 | "devDependencies": { 12 | "@symfony/webpack-encore": "^0.22.0", 13 | "node-sass": "^4.10.0", 14 | "sass-loader": "^7.1.0", 15 | "webpack-notifier": "^1.6.0" 16 | }, 17 | "license": "MIT", 18 | "private": true, 19 | "scripts": { 20 | "dev-server": "encore dev-server", 21 | "dev": "encore dev", 22 | "watch": "encore dev --watch", 23 | "build": "encore production --progress" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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 | src 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pifaace/symfony-blog/2c28217e824a8e048750ff87f598e883e225385a/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handle($request); 25 | $response->send(); 26 | $kernel->terminate($request, $response); 27 | -------------------------------------------------------------------------------- /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 | Disallow: 6 | -------------------------------------------------------------------------------- /src/Controller/Admin/AdminController.php: -------------------------------------------------------------------------------- 1 | render('backoffice/dashboard/dashboard.html.twig', [ 20 | 'countArticles' => $article->countArticles(), 21 | 'countComments' => $comment->countComments(), 22 | 'countUsers' => $user->countUsers(), 23 | ]); 24 | } 25 | 26 | /** 27 | * @Route("/admin/articles", name="admin-articles", methods={"GET"}) 28 | */ 29 | public function listArticle(ArticleRepository $articleRepository): Response 30 | { 31 | return $this->render('backoffice/article/list.html.twig', [ 32 | 'articles' => $articleRepository->getArticlesWithComment(), 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Controller/Admin/ArticleController.php: -------------------------------------------------------------------------------- 1 | articleManager = $articleManager; 32 | $this->trans = $trans; 33 | } 34 | 35 | /** 36 | * @Route("admin/article/new", name="article_new", methods={"GET", "POST"}) 37 | */ 38 | public function new(Request $request, FlashMessage $flashMessage, Notifier $notifier, Publisher $publisher): Response 39 | { 40 | $article = new Article(); 41 | $form = $this->createForm(ArticleType::class, $article); 42 | 43 | $form->handleRequest($request); 44 | 45 | if ($form->isSubmitted() && $form->isValid()) { 46 | $this->articleManager->create($article); 47 | 48 | $notifier->articleCreated($article, $publisher); 49 | 50 | $flashMessage->createMessage( 51 | $request, 52 | FlashMessage::INFO_MESSAGE, 53 | $this->trans->trans('backoffice.articles.flashmessage_publish')); 54 | 55 | return $this->redirectToRoute('admin-articles'); 56 | } 57 | 58 | return $this->render('backoffice/article/add.html.twig', [ 59 | 'form' => $form->createView(), 60 | ]); 61 | } 62 | 63 | /** 64 | * @Route("admin/article/{slug}/edit", name="article_edit", methods={"GET", "POST"}) 65 | */ 66 | public function edit(Request $request, Article $article, FlashMessage $flashMessage): Response 67 | { 68 | $image = $article->getImage(); 69 | $form = $this->createForm(ArticleType::class, $article); 70 | 71 | $form->handleRequest($request); 72 | if ($form->isSubmitted() && $form->isValid()) { 73 | $this->articleManager->edit($article); 74 | 75 | $flashMessage->createMessage( 76 | $request, 77 | FlashMessage::INFO_MESSAGE, 78 | $this->trans->trans('backoffice.articles.flashmessage_edit') 79 | ); 80 | 81 | return $this->redirectToRoute('article_edit', ['slug' => $article->getSlug()]); 82 | } 83 | 84 | return $this->render('backoffice/article/edit.html.twig', [ 85 | 'article' => $article, 86 | 'form' => $form->createView(), 87 | 'currentImage' => $image, 88 | ]); 89 | } 90 | 91 | /** 92 | * @Route("admin/article/{slug}/delete", name="article_delete", methods={"GET", "POST"}) 93 | */ 94 | public function delete(Request $request, Article $article, FlashMessage $flashMessage): Response 95 | { 96 | $this->articleManager->remove($article); 97 | 98 | $flashMessage->createMessage( 99 | $request, 100 | FlashMessage::INFO_MESSAGE, 101 | $this->trans->trans('backoffice.articles.flashmessage_deleted_article') 102 | ); 103 | 104 | return $this->redirectToRoute('admin-articles'); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Controller/BlogController.php: -------------------------------------------------------------------------------- 1 | getPage(); 26 | $articles = $paginator->getItemList($articleRepository, $page); 27 | $nbPages = $paginator->countPage($articles); 28 | 29 | return $this->render('blog/home/index.html.twig', [ 30 | 'articles' => $articles, 31 | 'nbPages' => $nbPages, 32 | 'page' => $page, 33 | ]); 34 | } 35 | 36 | /** 37 | * @Route("article/{slug}", name="article_show", methods={"GET", "POST"}) 38 | */ 39 | public function show(Article $article): Response 40 | { 41 | return $this->render('blog/article/show.html.twig', [ 42 | 'article' => $article, 43 | ]); 44 | } 45 | 46 | /** 47 | * @Route("article/{slug}/comment/new", name="comment_new", methods={"POST"}) 48 | */ 49 | public function newComment(Request $request, Article $article): Response 50 | { 51 | $comment = new Comment(); 52 | $form = $this->createForm(CommentType::class, $comment); 53 | 54 | $form->handleRequest($request); 55 | if ($form->isSubmitted() && $form->isValid()) { 56 | $em = $this->getDoctrine()->getManager(); 57 | $user = $this->getUser(); 58 | $article->addComment($comment); 59 | $user->addComment($comment); 60 | 61 | $em->persist($comment); 62 | $em->flush(); 63 | } 64 | 65 | return $this->redirectToRoute('article_show', ['slug' => $article->getSlug()]); 66 | } 67 | 68 | /** 69 | * @ParamConverter() 70 | */ 71 | public function commentForm(Article $article): Response 72 | { 73 | $form = $this->createForm(CommentType::class); 74 | 75 | return $this->render('blog/article/_comment_form.html.twig', [ 76 | 'form' => $form->createView(), 77 | 'article' => $article, 78 | ]); 79 | } 80 | 81 | /** 82 | * @Route("/switch-locale/{locale}", name="switch_locale", methods={"GET"}) 83 | */ 84 | public function switchLocale(Request $request, string $locale) 85 | { 86 | $request->getSession()->set('locale', $locale); 87 | 88 | return $this->redirectToRoute('homepage'); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Controller/NotificationController.php: -------------------------------------------------------------------------------- 1 | isXmlHttpRequest()) { 25 | $unreadNotifications = $userNotificationRepository->findBy(['user' => $user, 'isRead' => false]); 26 | 27 | if (!empty($unreadNotifications)) { 28 | foreach ($unreadNotifications as $userNotification) { 29 | $userNotification->setIsRead(true); 30 | } 31 | 32 | $em->flush(); 33 | 34 | return new Response('', 204); 35 | } 36 | } 37 | 38 | return new Response('Invalid request'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Controller/UserSettingsController.php: -------------------------------------------------------------------------------- 1 | render('blog/user/profile/show.html.twig', [ 20 | 'user' => $this->getUser(), 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Entity/Comment.php: -------------------------------------------------------------------------------- 1 | id; 58 | } 59 | 60 | public function setContent(string $content): self 61 | { 62 | $this->content = $content; 63 | 64 | return $this; 65 | } 66 | 67 | public function getContent(): ?string 68 | { 69 | return $this->content; 70 | } 71 | 72 | /** 73 | * @ORM\PrePersist() 74 | */ 75 | public function setCreateAt() 76 | { 77 | $this->createAt = new \DateTime(); 78 | } 79 | 80 | public function getCreateAt(): \DateTime 81 | { 82 | return $this->createAt; 83 | } 84 | 85 | public function setArticle(Article $article): self 86 | { 87 | $this->article = $article; 88 | 89 | return $this; 90 | } 91 | 92 | public function getArticle(): Article 93 | { 94 | return $this->article; 95 | } 96 | 97 | public function getUser(): ?User 98 | { 99 | return $this->user; 100 | } 101 | 102 | public function setUser(User $user): void 103 | { 104 | $this->user = $user; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Entity/Image.php: -------------------------------------------------------------------------------- 1 | id; 49 | } 50 | 51 | public function getFile(): ?UploadedFile 52 | { 53 | return $this->file; 54 | } 55 | 56 | public function setFile(UploadedFile $file = null): self 57 | { 58 | $this->file = $file; 59 | 60 | return $this; 61 | } 62 | 63 | public function setAlt(string $alt): self 64 | { 65 | $this->alt = $alt; 66 | 67 | return $this; 68 | } 69 | 70 | public function getAlt(): string 71 | { 72 | return $this->alt; 73 | } 74 | 75 | /** 76 | * @return bool 77 | */ 78 | public function isDeletedImage(): ?bool 79 | { 80 | return $this->deletedImage; 81 | } 82 | 83 | public function setDeletedImage(bool $deletedImage): void 84 | { 85 | $this->deletedImage = $deletedImage; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Entity/Notification.php: -------------------------------------------------------------------------------- 1 | userNotifications = new ArrayCollection(); 56 | } 57 | 58 | public function getId(): int 59 | { 60 | return $this->id; 61 | } 62 | 63 | public function getCreatedAt(): ?\DateTimeInterface 64 | { 65 | return $this->createdAt; 66 | } 67 | 68 | /** 69 | * @ORM\PrePersist 70 | */ 71 | public function setCreatedAt(): self 72 | { 73 | $this->createdAt = new \DateTime(); 74 | 75 | return $this; 76 | } 77 | 78 | public function getNotificationType(): NotificationType 79 | { 80 | return $this->notificationType; 81 | } 82 | 83 | public function setNotificationType(NotificationType $notificationType): self 84 | { 85 | $this->notificationType = $notificationType; 86 | 87 | return $this; 88 | } 89 | 90 | public function getCreatedBy(): User 91 | { 92 | return $this->createdBy; 93 | } 94 | 95 | public function setCreatedBy(UserInterface $createdBy): self 96 | { 97 | $this->createdBy = $createdBy; 98 | 99 | return $this; 100 | } 101 | 102 | public function getTargetLink(): string 103 | { 104 | return $this->targetLink; 105 | } 106 | 107 | public function setTargetLink(string $targetLink): self 108 | { 109 | $this->targetLink = $targetLink; 110 | 111 | return $this; 112 | } 113 | 114 | public function addUserNotification(UserNotification $userNotification): void 115 | { 116 | $userNotification->setNotification($this); 117 | if (!$this->userNotifications->contains($userNotification)) { 118 | $this->userNotifications->add($userNotification); 119 | } 120 | } 121 | 122 | public function removeUserNotification(UserNotification $userNotification): void 123 | { 124 | $this->userNotifications->removeElement($userNotification); 125 | } 126 | 127 | public function getComments(): ?Collection 128 | { 129 | return $this->userNotifications; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Entity/NotificationType.php: -------------------------------------------------------------------------------- 1 | id; 29 | } 30 | 31 | public function getName(): ?string 32 | { 33 | return $this->name; 34 | } 35 | 36 | public function setName(string $name): self 37 | { 38 | $this->name = $name; 39 | 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Entity/Tag.php: -------------------------------------------------------------------------------- 1 | id; 32 | } 33 | 34 | public function setName(string $name): self 35 | { 36 | $this->name = $name; 37 | 38 | return $this; 39 | } 40 | 41 | public function getName(): string 42 | { 43 | return $this->name; 44 | } 45 | 46 | public function __toString(): string 47 | { 48 | return $this->getName(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Entity/UserNotification.php: -------------------------------------------------------------------------------- 1 | isRead = false; 45 | } 46 | 47 | public function getId(): ?int 48 | { 49 | return $this->id; 50 | } 51 | 52 | public function getIsRead(): ?bool 53 | { 54 | return $this->isRead; 55 | } 56 | 57 | public function setIsRead(bool $isRead): self 58 | { 59 | $this->isRead = $isRead; 60 | 61 | return $this; 62 | } 63 | 64 | public function getNotification(): Notification 65 | { 66 | return $this->notification; 67 | } 68 | 69 | public function setNotification(Notification $notification): void 70 | { 71 | $this->notification = $notification; 72 | } 73 | 74 | public function getUser(): User 75 | { 76 | return $this->user; 77 | } 78 | 79 | public function setUser(User $user): void 80 | { 81 | $this->user = $user; 82 | } 83 | 84 | public function getCreatedAt(): \DateTime 85 | { 86 | return $this->createdAt; 87 | } 88 | 89 | /** 90 | * @ORM\PrePersist 91 | */ 92 | public function setCreatedAt(): void 93 | { 94 | $this->createdAt = new \DateTime(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/EventSubscriber/PasswordTokenReset.php: -------------------------------------------------------------------------------- 1 | getSubject(); 16 | if (!$user instanceof User && null === $user->getResetPasswordToken()) { 17 | return; 18 | } 19 | $user->setResetPasswordToken(null); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/EventSubscriber/PreferredLocaleSubscriber.php: -------------------------------------------------------------------------------- 1 | supportedLocales = $supportedLocales; 24 | $this->defaultLocale = $locale; 25 | } 26 | 27 | public static function getSubscribedEvents() 28 | { 29 | return [ 30 | KernelEvents::REQUEST => [['onKernelRequest', 20]], 31 | ]; 32 | } 33 | 34 | public function onKernelRequest(GetResponseEvent $event) 35 | { 36 | $request = $event->getRequest(); 37 | $localeNeeded = $request->getSession()->get('locale'); 38 | 39 | if (in_array($localeNeeded, $this->supportedLocales)) { 40 | $request->setLocale($localeNeeded); 41 | } else { 42 | $request->setLocale($this->defaultLocale); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/EventSubscriber/SetMercureCookieSubscriber.php: -------------------------------------------------------------------------------- 1 | cookieGenerator = $cookieGenerator; 26 | $this->tokenStorage = $tokenStorage; 27 | } 28 | 29 | public static function getSubscribedEvents() 30 | { 31 | return [ 32 | 'kernel.response' => 'onKernelResponse', 33 | ]; 34 | } 35 | 36 | public function onKernelResponse(ResponseEvent $event) 37 | { 38 | if (null === $this->tokenStorage->getToken() || !$this->tokenStorage->getToken()->getUser() instanceof User) { 39 | return; 40 | } 41 | 42 | $cookie = $this->cookieGenerator->generate(); 43 | 44 | $event->getResponse()->headers->set( 45 | 'set-cookie', 46 | $cookie 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/EventSubscriber/UserActionSubscriber.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 20 | } 21 | 22 | public function getSubscribedEvents(): array 23 | { 24 | return [ 25 | Events::postPersist, 26 | Events::postUpdate, 27 | ]; 28 | } 29 | 30 | public function postPersist(LifecycleEventArgs $args): void 31 | { 32 | $this->logger->userAction(\get_class($args->getObject()), 'created'); 33 | } 34 | 35 | public function postUpdate(LifecycleEventArgs $args): void 36 | { 37 | $this->logger->userAction(\get_class($args->getObject()), 'updated'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Events.php: -------------------------------------------------------------------------------- 1 | add('title', TextType::class, ['label' => false]) 21 | ->add('content', TextareaType::class, ['label' => false]) 22 | ->add('image', ImageType::class, [ 23 | 'required' => false, 24 | 'label' => false, 25 | ]) 26 | ->add('tags', TagsType::class, ['required' => false]); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function configureOptions(OptionsResolver $resolver) 33 | { 34 | $resolver->setDefaults([ 35 | 'data_class' => 'App\Entity\Article', 36 | ]); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function getBlockPrefix() 43 | { 44 | return 'App_article'; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Form/CommentType.php: -------------------------------------------------------------------------------- 1 | add('content', TextareaType::class); 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function configureOptions(OptionsResolver $resolver) 25 | { 26 | $resolver->setDefaults([ 27 | 'data_class' => 'App\Entity\Comment', 28 | ]); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function getBlockPrefix() 35 | { 36 | return 'App_comment'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Form/DataTransformer/TagsTransformer.php: -------------------------------------------------------------------------------- 1 | em = $em; 19 | } 20 | 21 | public function transform($value): string 22 | { 23 | return implode(', ', $value); 24 | } 25 | 26 | public function reverseTransform($string): array 27 | { 28 | $names = array_filter(array_unique(array_map('trim', explode(',', $string)))); 29 | 30 | $tags = $this->em->getRepository('App:Tag')->findBy([ 31 | 'name' => $names, 32 | ]); 33 | 34 | $newNames = array_diff($names, $tags); 35 | 36 | foreach ($newNames as $newName) { 37 | $tag = new Tag(); 38 | $tag->setName($newName); 39 | 40 | $tags[] = $tag; 41 | } 42 | 43 | return $tags; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Form/ImageType.php: -------------------------------------------------------------------------------- 1 | add('file', FileType::class, [ 21 | 'label' => false, 22 | 'required' => false, 23 | ]); 24 | 25 | $builder->addEventListener( 26 | FormEvents::PRE_SET_DATA, 27 | function (FormEvent $event) { 28 | $image = $event->getData(); 29 | 30 | if (null == $image) { 31 | return; 32 | } 33 | 34 | if (null != $image->getId()) { 35 | $event->getForm()->add('deletedImage', CheckboxType::class, [ 36 | 'required' => false, 37 | 'label' => false, 38 | 'attr' => [ 39 | 'hidden' => true, 40 | 'class' => 'delete-img-confirm', 41 | ], 42 | ]); 43 | } else { 44 | $event->getForm()->remove('deletedImage'); 45 | } 46 | } 47 | ); 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function configureOptions(OptionsResolver $resolver) 54 | { 55 | $resolver->setDefaults([ 56 | 'data_class' => 'App\Entity\Image', 57 | ]); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function getBlockPrefix() 64 | { 65 | return 'App_image'; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Form/LoginType.php: -------------------------------------------------------------------------------- 1 | add('username', TextType::class, ['label' => false]) 17 | ->add('password', PasswordType::class, ['label' => false]); 18 | } 19 | 20 | public function configureOptions(OptionsResolver $resolver) 21 | { 22 | $resolver->setDefaults([ 23 | 'csrf_token_id' => 'login_authenticate', 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Form/PasswordResetNewType.php: -------------------------------------------------------------------------------- 1 | add('plainPassword', RepeatedType::class, [ 18 | 'type' => PasswordType::class, 19 | 'invalid_message' => 'reset_password.same_password', 20 | ]); 21 | } 22 | 23 | public function configureOptions(OptionsResolver $resolver) 24 | { 25 | $resolver->setDefaults([ 26 | 'data_class' => User::class, 27 | ]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Form/PasswordResetRequestType.php: -------------------------------------------------------------------------------- 1 | add('email', EmailType::class, [ 17 | 'constraints' => new NotBlank(['message' => 'forgot_password.email_required']), 18 | ]) 19 | ; 20 | } 21 | 22 | public function configureOptions(OptionsResolver $resolver) 23 | { 24 | $resolver->setDefaults([ 25 | // uncomment if you want to bind to a class 26 | //'data_class' => PasswordReset::class, 27 | ]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Form/RegistrationType.php: -------------------------------------------------------------------------------- 1 | add('username', TextType::class) 20 | ->add('email', EmailType::class) 21 | ->add('plainPassword', RepeatedType::class, [ 22 | 'type' => PasswordType::class, 23 | 'invalid_message' => 'signin.same_password', 24 | ]) 25 | ; 26 | } 27 | 28 | public function configureOptions(OptionsResolver $resolver) 29 | { 30 | $resolver->setDefaults([ 31 | 'data_class' => User::class, 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Form/Type/TagsType.php: -------------------------------------------------------------------------------- 1 | em = $em; 22 | } 23 | 24 | public function buildForm(FormBuilderInterface $builder, array $options) 25 | { 26 | $builder 27 | ->addModelTransformer(new CollectionToArrayTransformer(), true) 28 | ->addModelTransformer(new TagsTransformer($this->em), true); 29 | } 30 | 31 | public function getParent() 32 | { 33 | return HiddenType::class; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | getProjectDir().'/var/cache/'.$this->environment; 20 | } 21 | 22 | public function getLogDir() 23 | { 24 | return $this->getProjectDir().'/var/log'; 25 | } 26 | 27 | public function registerBundles() 28 | { 29 | $contents = require $this->getProjectDir().'/config/bundles.php'; 30 | foreach ($contents as $class => $envs) { 31 | if (isset($envs['all']) || isset($envs[$this->environment])) { 32 | yield new $class(); 33 | } 34 | } 35 | } 36 | 37 | protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader) 38 | { 39 | $container->setParameter('container.autowiring.strict_mode', true); 40 | $container->setParameter('container.dumper.inline_class_loader', true); 41 | $confDir = $this->getProjectDir().'/config'; 42 | $loader->load($confDir.'/packages/*'.self::CONFIG_EXTS, 'glob'); 43 | if (is_dir($confDir.'/packages/'.$this->environment)) { 44 | $loader->load($confDir.'/packages/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob'); 45 | } 46 | $loader->load($confDir.'/services'.self::CONFIG_EXTS, 'glob'); 47 | $loader->load($confDir.'/services_'.$this->environment.self::CONFIG_EXTS, 'glob'); 48 | } 49 | 50 | protected function configureRoutes(RouteCollectionBuilder $routes) 51 | { 52 | $confDir = $this->getProjectDir().'/config'; 53 | if (is_dir($confDir.'/routes/')) { 54 | $routes->import($confDir.'/routes/*'.self::CONFIG_EXTS, '/', 'glob'); 55 | } 56 | if (is_dir($confDir.'/routes/'.$this->environment)) { 57 | $routes->import($confDir.'/routes/'.$this->environment.'/**/*'.self::CONFIG_EXTS, '/', 'glob'); 58 | } 59 | $routes->import($confDir.'/routes'.self::CONFIG_EXTS, '/', 'glob'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Migrations/Version20171224150255.php: -------------------------------------------------------------------------------- 1 | abortIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'mysql\'.'); 19 | 20 | $this->addSql('CREATE TABLE tag (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 21 | $this->addSql('CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, username VARCHAR(25) NOT NULL, password VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, role LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', UNIQUE INDEX UNIQ_8D93D649F85E0677 (username), UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 22 | $this->addSql('CREATE TABLE comment (id INT AUTO_INCREMENT NOT NULL, article_id INT NOT NULL, username VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, content VARCHAR(255) NOT NULL, createAt DATETIME NOT NULL, INDEX IDX_9474526C7294869C (article_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 23 | $this->addSql('CREATE TABLE article (id INT AUTO_INCREMENT NOT NULL, author_id INT NOT NULL, image_id INT DEFAULT NULL, title VARCHAR(255) NOT NULL, create_at DATETIME NOT NULL, content VARCHAR(255) NOT NULL, INDEX IDX_23A0E66F675F31B (author_id), UNIQUE INDEX UNIQ_23A0E663DA5256D (image_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 24 | $this->addSql('CREATE TABLE article_tag (article_id INT NOT NULL, tag_id INT NOT NULL, INDEX IDX_919694F97294869C (article_id), INDEX IDX_919694F9BAD26311 (tag_id), PRIMARY KEY(article_id, tag_id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 25 | $this->addSql('CREATE TABLE image (id INT AUTO_INCREMENT NOT NULL, alt VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 26 | $this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526C7294869C FOREIGN KEY (article_id) REFERENCES article (id)'); 27 | $this->addSql('ALTER TABLE article ADD CONSTRAINT FK_23A0E66F675F31B FOREIGN KEY (author_id) REFERENCES user (id)'); 28 | $this->addSql('ALTER TABLE article ADD CONSTRAINT FK_23A0E663DA5256D FOREIGN KEY (image_id) REFERENCES image (id)'); 29 | $this->addSql('ALTER TABLE article_tag ADD CONSTRAINT FK_919694F97294869C FOREIGN KEY (article_id) REFERENCES article (id) ON DELETE CASCADE'); 30 | $this->addSql('ALTER TABLE article_tag ADD CONSTRAINT FK_919694F9BAD26311 FOREIGN KEY (tag_id) REFERENCES tag (id) ON DELETE CASCADE'); 31 | } 32 | 33 | public function down(Schema $schema): void 34 | { 35 | // this down() migration is auto-generated, please modify it to your needs 36 | $this->abortIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'mysql\'.'); 37 | 38 | $this->addSql('ALTER TABLE article_tag DROP FOREIGN KEY FK_919694F9BAD26311'); 39 | $this->addSql('ALTER TABLE article DROP FOREIGN KEY FK_23A0E66F675F31B'); 40 | $this->addSql('ALTER TABLE comment DROP FOREIGN KEY FK_9474526C7294869C'); 41 | $this->addSql('ALTER TABLE article_tag DROP FOREIGN KEY FK_919694F97294869C'); 42 | $this->addSql('ALTER TABLE article DROP FOREIGN KEY FK_23A0E663DA5256D'); 43 | $this->addSql('DROP TABLE tag'); 44 | $this->addSql('DROP TABLE user'); 45 | $this->addSql('DROP TABLE comment'); 46 | $this->addSql('DROP TABLE article'); 47 | $this->addSql('DROP TABLE article_tag'); 48 | $this->addSql('DROP TABLE image'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Migrations/Version20180209225711.php: -------------------------------------------------------------------------------- 1 | abortIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'mysql\'.'); 19 | 20 | $this->addSql('ALTER TABLE article CHANGE content content LONGTEXT NOT NULL'); 21 | } 22 | 23 | public function down(Schema $schema): void 24 | { 25 | // this down() migration is auto-generated, please modify it to your needs 26 | $this->abortIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'mysql\'.'); 27 | 28 | $this->addSql('ALTER TABLE article CHANGE content content VARCHAR(255) NOT NULL COLLATE utf8_unicode_ci'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Migrations/Version20180314174711.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('ALTER TABLE article ADD slug VARCHAR(255) NOT NULL'); 19 | $this->addSql('CREATE UNIQUE INDEX UNIQ_23A0E66989D9B62 ON article (slug)'); 20 | } 21 | 22 | public function down(Schema $schema): void 23 | { 24 | // this down() migration is auto-generated, please modify it to your needs 25 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 26 | 27 | $this->addSql('DROP INDEX UNIQ_23A0E66989D9B62 ON article'); 28 | $this->addSql('ALTER TABLE article DROP slug'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Migrations/Version20180402143231.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('ALTER TABLE user ADD reset_password_token VARCHAR(255) DEFAULT NULL'); 19 | $this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649452C9EC5 ON user (reset_password_token)'); 20 | } 21 | 22 | public function down(Schema $schema): void 23 | { 24 | // this down() migration is auto-generated, please modify it to your needs 25 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 26 | 27 | $this->addSql('DROP INDEX UNIQ_8D93D649452C9EC5 ON user'); 28 | $this->addSql('ALTER TABLE user DROP reset_password_token'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Migrations/Version20180423195809.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('ALTER TABLE user ADD token_created_at DATETIME DEFAULT NULL'); 19 | } 20 | 21 | public function down(Schema $schema): void 22 | { 23 | // this down() migration is auto-generated, please modify it to your needs 24 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 25 | 26 | $this->addSql('ALTER TABLE user DROP token_created_at'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Migrations/Version20180513194510.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('ALTER TABLE user CHANGE token_created_at token_expiration_date DATETIME DEFAULT NULL'); 19 | } 20 | 21 | public function down(Schema $schema): void 22 | { 23 | // this down() migration is auto-generated, please modify it to your needs 24 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 25 | 26 | $this->addSql('ALTER TABLE user CHANGE token_expiration_date token_created_at DATETIME DEFAULT NULL'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Migrations/Version20180607202731.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('ALTER TABLE user CHANGE password password VARCHAR(255) DEFAULT NULL'); 19 | } 20 | 21 | public function down(Schema $schema) : void 22 | { 23 | // this down() migration is auto-generated, please modify it to your needs 24 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 25 | 26 | $this->addSql('ALTER TABLE user CHANGE password password VARCHAR(255) NOT NULL COLLATE utf8_unicode_ci'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Migrations/Version20180607203541.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('ALTER TABLE user ADD provider_id INT DEFAULT NULL'); 19 | } 20 | 21 | public function down(Schema $schema) : void 22 | { 23 | // this down() migration is auto-generated, please modify it to your needs 24 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 25 | 26 | $this->addSql('ALTER TABLE user DROP provider_id'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Migrations/Version20180611080116.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('ALTER TABLE comment ADD user_id INT NOT NULL, DROP username, DROP email'); 19 | $this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526CA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); 20 | $this->addSql('CREATE INDEX IDX_9474526CA76ED395 ON comment (user_id)'); 21 | } 22 | 23 | public function down(Schema $schema) : void 24 | { 25 | // this down() migration is auto-generated, please modify it to your needs 26 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 27 | 28 | $this->addSql('ALTER TABLE comment DROP FOREIGN KEY FK_9474526CA76ED395'); 29 | $this->addSql('DROP INDEX IDX_9474526CA76ED395 ON comment'); 30 | $this->addSql('ALTER TABLE comment ADD username VARCHAR(255) NOT NULL COLLATE utf8_unicode_ci, ADD email VARCHAR(255) NOT NULL COLLATE utf8_unicode_ci, DROP user_id'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Migrations/Version20181224152453.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('CREATE TABLE child_comment (id INT AUTO_INCREMENT NOT NULL, comment_id INT NOT NULL, user_id INT NOT NULL, content LONGTEXT NOT NULL, create_at DATETIME NOT NULL, INDEX IDX_F6C01A77F8697D13 (comment_id), INDEX IDX_F6C01A77A76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); 19 | $this->addSql('ALTER TABLE child_comment ADD CONSTRAINT FK_F6C01A77F8697D13 FOREIGN KEY (comment_id) REFERENCES comment (id)'); 20 | $this->addSql('ALTER TABLE child_comment ADD CONSTRAINT FK_F6C01A77A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); 21 | $this->addSql('ALTER TABLE article DROP FOREIGN KEY FK_23A0E66F675F31B'); 22 | $this->addSql('DROP INDEX IDX_23A0E66F675F31B ON article'); 23 | $this->addSql('ALTER TABLE article CHANGE author_id user_id INT NOT NULL'); 24 | $this->addSql('ALTER TABLE article ADD CONSTRAINT FK_23A0E66A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); 25 | $this->addSql('CREATE INDEX IDX_23A0E66A76ED395 ON article (user_id)'); 26 | $this->addSql('ALTER TABLE comment CHANGE content content LONGTEXT NOT NULL'); 27 | } 28 | 29 | public function down(Schema $schema) : void 30 | { 31 | // this down() migration is auto-generated, please modify it to your needs 32 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 33 | 34 | $this->addSql('DROP TABLE child_comment'); 35 | $this->addSql('ALTER TABLE article DROP FOREIGN KEY FK_23A0E66A76ED395'); 36 | $this->addSql('DROP INDEX IDX_23A0E66A76ED395 ON article'); 37 | $this->addSql('ALTER TABLE article CHANGE user_id author_id INT NOT NULL'); 38 | $this->addSql('ALTER TABLE article ADD CONSTRAINT FK_23A0E66F675F31B FOREIGN KEY (author_id) REFERENCES user (id)'); 39 | $this->addSql('CREATE INDEX IDX_23A0E66F675F31B ON article (author_id)'); 40 | $this->addSql('ALTER TABLE comment CHANGE content content VARCHAR(255) NOT NULL COLLATE utf8_unicode_ci'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Migrations/Version20190913132213.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('CREATE TABLE notification (id INT AUTO_INCREMENT NOT NULL, notification_type_id INT NOT NULL, created_by_id INT NOT NULL, created_at DATETIME NOT NULL, target_link VARCHAR(255) NOT NULL, INDEX IDX_BF5476CAD0520624 (notification_type_id), INDEX IDX_BF5476CAB03A8386 (created_by_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); 19 | $this->addSql('CREATE TABLE notification_type (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); 20 | $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAD0520624 FOREIGN KEY (notification_type_id) REFERENCES notification_type (id)'); 21 | $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAB03A8386 FOREIGN KEY (created_by_id) REFERENCES user (id)'); 22 | $this->addSql('DROP TABLE child_comment'); 23 | } 24 | 25 | public function down(Schema $schema) : void 26 | { 27 | // this down() migration is auto-generated, please modify it to your needs 28 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 29 | 30 | $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAD0520624'); 31 | $this->addSql('CREATE TABLE child_comment (id INT AUTO_INCREMENT NOT NULL, comment_id INT NOT NULL, user_id INT NOT NULL, content LONGTEXT NOT NULL COLLATE utf8mb4_unicode_ci, create_at DATETIME NOT NULL, INDEX IDX_F6C01A77A76ED395 (user_id), INDEX IDX_F6C01A77F8697D13 (comment_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB COMMENT = \'\' '); 32 | $this->addSql('ALTER TABLE child_comment ADD CONSTRAINT FK_F6C01A77A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); 33 | $this->addSql('ALTER TABLE child_comment ADD CONSTRAINT FK_F6C01A77F8697D13 FOREIGN KEY (comment_id) REFERENCES comment (id)'); 34 | $this->addSql('DROP TABLE notification'); 35 | $this->addSql('DROP TABLE notification_type'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Migrations/Version20190929114309.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('CREATE TABLE user_notification (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, notification_id INT DEFAULT NULL, is_read TINYINT(1) NOT NULL, created_at DATETIME NOT NULL, INDEX IDX_3F980AC8A76ED395 (user_id), INDEX IDX_3F980AC8EF1A9D84 (notification_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); 19 | $this->addSql('ALTER TABLE user_notification ADD CONSTRAINT FK_3F980AC8A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); 20 | $this->addSql('ALTER TABLE user_notification ADD CONSTRAINT FK_3F980AC8EF1A9D84 FOREIGN KEY (notification_id) REFERENCES notification (id)'); 21 | } 22 | 23 | public function down(Schema $schema) : void 24 | { 25 | // this down() migration is auto-generated, please modify it to your needs 26 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 27 | 28 | $this->addSql('DROP TABLE user_notification'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Repository/ArticleRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('p'); 24 | 25 | $qb 26 | ->setFirstResult(($page - 1) * $maxResults) 27 | ->setMaxResults($maxResults) 28 | ->orderBy('p.createAt', 'DESC'); 29 | 30 | return new Paginator($qb); 31 | } 32 | 33 | public function getArticlesWithComment(): array 34 | { 35 | return $this->createQueryBuilder('a') 36 | ->leftJoin('a.comments', 'c') 37 | ->addSelect('c') 38 | ->orderBy('a.createAt', 'DESC') 39 | ->getQuery()->getResult(); 40 | } 41 | 42 | public function countArticles(): string 43 | { 44 | return $this->createQueryBuilder('a') 45 | ->select('COUNT(a)') 46 | ->getQuery() 47 | ->getSingleScalarResult(); 48 | } 49 | 50 | public function saveNewArticle(Article $article): void 51 | { 52 | $this->_em->persist($article); 53 | $this->_em->flush(); 54 | } 55 | 56 | public function saveExistingArticle(): void 57 | { 58 | $this->_em->flush(); 59 | } 60 | 61 | public function remove(Article $article): void 62 | { 63 | $this->_em->remove($article); 64 | $this->_em->flush(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Repository/CommentRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('c') 19 | ->select('COUNT(c)') 20 | ->getQuery() 21 | ->getSingleScalarResult(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Repository/ImageRepository.php: -------------------------------------------------------------------------------- 1 | _em->persist($notification); 26 | $this->_em->flush(); 27 | } 28 | 29 | /** 30 | * Get unread notifications for the current user. 31 | */ 32 | public function getUnreadNotification(User $user) 33 | { 34 | $qb = $this->createQueryBuilder('n'); 35 | $qb 36 | ->innerJoin('n.userNotifications', 'un') 37 | ->where('un.isRead = 0') 38 | ->andWhere('un.user = :user') 39 | ->setParameter(':user', $user) 40 | ->orderBy('n.createdAt', 'DESC') 41 | ; 42 | 43 | return $qb->getQuery()->getResult(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Repository/NotificationTypeRepository.php: -------------------------------------------------------------------------------- 1 | _em->persist($userNotification); 25 | } 26 | 27 | public function flush() 28 | { 29 | $this->_em->flush(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Repository/UserRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('u'); 19 | $qb->where('u.resetPasswordToken = :token'); 20 | $qb->setParameter(':token', $token); 21 | 22 | return $qb->getQuery()->getOneOrNullResult(); 23 | } 24 | 25 | public function getByProviderId(string $providerId): ?User 26 | { 27 | $qb = $this->createQueryBuilder('u'); 28 | $qb 29 | ->where('u.providerId = :providerId') 30 | ->setParameter(':providerId', $providerId); 31 | 32 | return $qb->getQuery()->getOneOrNullResult(); 33 | } 34 | 35 | public function countUsers(): string 36 | { 37 | return $this->createQueryBuilder('u') 38 | ->select('COUNT(u)') 39 | ->getQuery() 40 | ->getSingleScalarResult(); 41 | } 42 | 43 | public function save(User $user): void 44 | { 45 | $this->_em->persist($user); 46 | $this->_em->flush(); 47 | } 48 | 49 | public function saveNewPassword() 50 | { 51 | $this->_em->flush(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Services/Article/Manager/ArticleManager.php: -------------------------------------------------------------------------------- 1 | tokenStorage = $tokenStorage; 40 | $this->uploader = $uploader; 41 | $this->repository = $repository; 42 | $this->em = $em; 43 | } 44 | 45 | public function create(Article $article): void 46 | { 47 | $article->setAuthor($this->tokenStorage->getToken()->getUser()); 48 | 49 | if (null !== $article->getImage()) { 50 | if ($this->uploader->hasNewImage($article->getImage())) { 51 | $this->uploadImage($article); 52 | } 53 | } 54 | 55 | $this->repository->saveNewArticle($article); 56 | } 57 | 58 | public function edit(Article $article) 59 | { 60 | $image = $article->getImage(); 61 | 62 | if (null !== $image) { 63 | if ($this->uploader->hasNewImage($image)) { 64 | if ($this->uploader->hasActiveImage($image)) { 65 | $this->uploader->removeImage($image->getAlt()); 66 | } 67 | $this->uploadImage($article); 68 | } else { 69 | if ($this->uploader->hasActiveImage($image) && $this->uploader->isDeleteImageChecked($image)) { 70 | $this->uploader->removeImage($image->getAlt()); 71 | $this->em->remove($image); 72 | $article->setImage(null); 73 | } 74 | } 75 | } 76 | 77 | $this->repository->saveExistingArticle(); 78 | } 79 | 80 | public function remove(Article $article): void 81 | { 82 | $this->repository->remove($article); 83 | } 84 | 85 | private function uploadImage(Article $article): void 86 | { 87 | $alt = $this->uploader->generateAlt($article->getImage()->getFile()); 88 | $article->getImage()->setAlt($alt); 89 | $this->uploader->uploadImage($article->getImage()->getFile(), $alt); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Services/Faker/Provider/EncodePasswordProvider.php: -------------------------------------------------------------------------------- 1 | encoder = $encoder; 18 | } 19 | 20 | public function encodePassword(User $user, string $plainPassword) 21 | { 22 | return $this->encoder->encodePassword($user, $plainPassword); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Services/FlashMessage.php: -------------------------------------------------------------------------------- 1 | getSession()->getFlashBag()->add($type, $message); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Services/Mailer.php: -------------------------------------------------------------------------------- 1 | mailer = $mailer; 15 | } 16 | 17 | public function buildAndSendMail(string $subject, $recever, $body) 18 | { 19 | $message = (new \Swift_Message($subject)) 20 | ->setFrom('no-remply@symfony-blog.com') 21 | ->setTo($recever) 22 | ->setBody($body, 'text/html'); 23 | 24 | $this->send($message); 25 | } 26 | 27 | private function send($message) 28 | { 29 | $this->mailer->send($message); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Services/MercureCookieGenerator.php: -------------------------------------------------------------------------------- 1 | secret = $secret; 20 | } 21 | 22 | public function generate(): string 23 | { 24 | $token = (new Builder()) 25 | ->set('mercure', ['subscribe' => ['http://symfony-blog.fr/group/users']]) 26 | ->sign(new Sha256(), $this->secret) 27 | ->getToken(); 28 | 29 | return sprintf('%s=%s; path=hub/; httponly;', self::MERCURE_AUTHORIZATION_HEADER, $token); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Services/Notification/Factory/NotificationFactory.php: -------------------------------------------------------------------------------- 1 | notificationTypeRepository = $notificationTypeRepository; 33 | $this->storage = $storage; 34 | $this->urlGenerator = $urlGenerator; 35 | } 36 | 37 | public function create(Article $article, string $notificationTypeName): Notification 38 | { 39 | $notificationType = $this->notificationTypeRepository->findOneBy(['name' => $notificationTypeName]); 40 | $url = $this->urlGenerator->generate('article_show', ['slug' => $article->getSlug()]); 41 | 42 | $notification = (new Notification()) 43 | ->setNotificationType($notificationType) 44 | ->setCreatedBy($this->storage->getToken()->getUser()) 45 | ->setTargetLink($url); 46 | 47 | return $notification; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Services/Notifier.php: -------------------------------------------------------------------------------- 1 | notificationFactory = $notificationFactory; 60 | $this->notificationRepository = $notificationRepository; 61 | $this->serializer = $serializer; 62 | $this->userRepository = $userRepository; 63 | $this->userNotificationFactory = $userNotificationFactory; 64 | $this->userNotificationRepository = $userNotificationRepository; 65 | } 66 | 67 | /** 68 | * When an article is created, the app will notify all users that are connected. 69 | */ 70 | public function articleCreated(Article $article, Publisher $publisher) 71 | { 72 | $notification = $this->notificationFactory->create($article, NotificationType::ARTICLE_CREATED); 73 | $this->notificationRepository->save($notification); 74 | 75 | $jsonContent = $this->serializer->serialize($notification, 'json'); 76 | 77 | $users = $this->userRepository->findAll(); 78 | 79 | foreach ($users as $user) { 80 | $userNotification = $this->userNotificationFactory->create($notification, $user); 81 | $this->userNotificationRepository->persist($userNotification); 82 | } 83 | 84 | $this->userNotificationRepository->flush(); 85 | 86 | $update = new Update( 87 | 'http://symfony-blog.fr/new/article', 88 | $jsonContent, 89 | ['http://symfony-blog.fr/group/users'] 90 | ); 91 | 92 | $publisher($update); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Services/Paginator.php: -------------------------------------------------------------------------------- 1 | itemPerPage = $itemPerPage; 22 | $this->requestStack = $requestStack; 23 | } 24 | 25 | public function getItemList($repository, $page) 26 | { 27 | return $repository->paginator($page, $this->itemPerPage); 28 | } 29 | 30 | public function countPage($items): int 31 | { 32 | return ceil(\count($items) / $this->itemPerPage); 33 | } 34 | 35 | public function getPage(): int 36 | { 37 | $request = $this->requestStack->getCurrentRequest(); 38 | 39 | $page = $request->query->get('page'); 40 | 41 | if ($page < 1) { 42 | $page = 1; 43 | } 44 | 45 | return $page; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Services/TokenPassword.php: -------------------------------------------------------------------------------- 1 | em = $em; 35 | $this->generator = $generator; 36 | } 37 | 38 | /** 39 | * Generate and add a temporary token to the target user to allow a reset password. 40 | */ 41 | public function addToken(User $user): void 42 | { 43 | $this->token = $this->generateToken(); 44 | $user->setResetPasswordToken($this->token); 45 | $user->setTokenExpirationDate(); 46 | $this->em->flush(); 47 | } 48 | 49 | private function generateToken(): string 50 | { 51 | return $this->generator->generateToken(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Services/Uploader.php: -------------------------------------------------------------------------------- 1 | targetDir = $targetDir; 16 | } 17 | 18 | public function hasNewImage(Image $image): bool 19 | { 20 | return null !== $image->getFile() && $image->getFile() instanceof UploadedFile; 21 | } 22 | 23 | public function hasActiveImage(Image $image): bool 24 | { 25 | return null !== $image->getId(); 26 | } 27 | 28 | public function isDeleteImageChecked(Image $image): bool 29 | { 30 | return $image->isDeletedImage(); 31 | } 32 | 33 | public function generateAlt(UploadedFile $file): string 34 | { 35 | return md5(uniqid('', true)).'.'.$file->guessExtension(); 36 | } 37 | 38 | public function uploadImage(UploadedFile $file, string $imageName): void 39 | { 40 | $file->move($this->getTargetDir(), $imageName); 41 | } 42 | 43 | public function removeImage(string $imageName): void 44 | { 45 | $fs = new Filesystem(); 46 | 47 | if ($fs->exists($this->getTargetDir().$imageName)) { 48 | unlink($this->getTargetDir().$imageName); 49 | } 50 | } 51 | 52 | private function getTargetDir(): string 53 | { 54 | return $this->targetDir; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Services/User/Manager/UserManager.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 70 | $this->checker = $checker; 71 | $this->eventDispatcher = $eventDispatcher; 72 | $this->mailer = $mailer; 73 | $this->trans = $trans; 74 | $this->templating = $templating; 75 | $this->requestStack = $requestStack; 76 | $this->encoder = $encoder; 77 | } 78 | 79 | public function create(User $user): void 80 | { 81 | $user->setPassword($this->encodePassword($user)); 82 | $this->repository->save($user); 83 | } 84 | 85 | public function resetPassword(User $user): void 86 | { 87 | $genericEvent = new GenericEvent($user); 88 | $this->eventDispatcher->dispatch(Events::TOKEN_RESET, $genericEvent); 89 | 90 | $user->setPassword($this->encodePassword($user)); 91 | $this->repository->saveNewPassword(); 92 | } 93 | 94 | public function sendPasswordRequestEmail(User $user) 95 | { 96 | $this->mailer->buildAndSendMail( 97 | $this->trans->trans('reset_password.title', [], 'emails'), 98 | $user->getEmail(), 99 | $this 100 | ->templating 101 | ->render('email/password_request/_password_reset_email_'. 102 | $this->requestStack->getMasterRequest()->getLocale().'.html.twig', [ 103 | 'username' => $user->getUsername(), 104 | 'token' => $user->getResetPasswordToken(), 105 | ]) 106 | ); 107 | } 108 | 109 | public function isLogin(): bool 110 | { 111 | return $this->checker->isGranted('IS_AUTHENTICATED_FULLY'); 112 | } 113 | 114 | public function isTokenExpired(User $user): bool 115 | { 116 | return $user->getTokenExpirationDate() < new \DateTime(); 117 | } 118 | 119 | private function encodePassword(User $user): string 120 | { 121 | return $this->encoder->encodePassword($user, $user->getPlainPassword()); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Services/UserActionLogger.php: -------------------------------------------------------------------------------- 1 | logger = $userActionLogger; 17 | } 18 | 19 | public function userAction(string $entityName, string $status): void 20 | { 21 | $this->logger->info('User '.$status.' : '.$entityName); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Services/UserNotification/Factory/UserNotificationFactory.php: -------------------------------------------------------------------------------- 1 | setUser($user); 15 | $notification->addUserNotification($userNotification); 16 | 17 | return $userNotification; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Twig/NotificationExtension.php: -------------------------------------------------------------------------------- 1 | notificationRepository = $notificationRepository; 26 | $this->tokenStorage = $tokenStorage; 27 | } 28 | 29 | /** 30 | * Returns unread notifications to the twig global variable 'notifications'. 31 | * 32 | * @return array 33 | */ 34 | public function getNotifications() 35 | { 36 | $user = $this->tokenStorage->getToken()->getUser(); 37 | $notifications = $this->notificationRepository->getUnreadNotification($user); 38 | 39 | return $notifications; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /templates/backoffice/article/add.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'backoffice/layout-backoffice.html.twig' %} 2 | 3 | {% block body %} 4 | {% set titlePage = 'backoffice.articles.add.title'|trans %} 5 | 6 |
7 |
8 |

{{ titlePage }}

9 |
10 | 11 | {{ form_start(form) }} 12 | {{ form_errors(form) }} 13 | 14 | {{ 'backoffice.articles.add_edit_form.actuality'|trans }} 15 |
16 | {{ form_label(form.title, 'backoffice.articles.add_edit_form.article_title'|trans, {'label_attr': {'class': 'label'}}) }} 17 |
18 | {{ form_widget(form.title, {'attr': {'class': 'input'}}) }} 19 |
20 |
21 | {{ form_errors(form.title) }} 22 |
23 |
24 | 25 |
26 | {{ form_label(form.content, 'backoffice.articles.add_edit_form.article_content'|trans, {'label_attr': {'class': 'label'}}) }} 27 |
28 | {{ form_widget(form.content, {'attr': {'class': 'textarea'}}) }} 29 |
30 |
31 | {{ form_errors(form.content) }} 32 |
33 |
34 | 35 |
36 | {{ form_label(form.tags, 'backoffice.articles.add_edit_form.article_tags'|trans, {'label_attr': {'class': 'label'}}) }} 37 |
38 | {{ form_widget(form.tags, {'attr': {'class': 'hidden-tag-input'}}) }} 39 |
40 | 41 |
42 |
43 |
44 | {{ form_errors(form.tags) }} 45 |
46 | {{ 'backoffice.articles.add_edit_form.article_tags_helper'|trans }} 47 |
48 | 49 | {{ 'backoffice.articles.add_edit_form.article_media'|trans }} 50 | 51 |
52 | {{ form_widget(form.image) }} 53 |
54 | {{ form_errors(form.image) }} 55 |
56 |
57 | 58 | {{ form_rest(form) }} 59 | 60 |
61 | 64 |
65 | {{ form_end(form) }} 66 |
67 | {% endblock %} 68 | 69 | {% block javascripts %} 70 | {{ parent() }} 71 | 72 | {% endblock %} 73 | -------------------------------------------------------------------------------- /templates/backoffice/article/list.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'backoffice/layout-backoffice.html.twig' %} 2 | 3 | {% block body %} 4 | {% set titlePage = 'backoffice.articles.list.title'|trans %} 5 |
6 |
7 |
8 |
9 |

{{ titlePage }}

10 |
11 |
12 |
13 | 14 | {% include 'flashMessage/flashMessage.html.twig' %} 15 | 16 |
17 | 18 | 25 | 26 |
27 |
28 | {% if articles is not empty %} 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {% for article in articles %} 41 | 42 | 43 | 44 | 45 | 46 | 61 | 62 | {% endfor %} 63 | 64 |
{{ 'backoffice.articles.list.article_title'|trans }}{{ 'backoffice.articles.list.article_author'|trans }}{{ 'backoffice.articles.list.article_posted_at'|trans }}{{ 'backoffice.articles.list.article_comment'|trans }}{{ 'backoffice.articles.list.articlr_action'|trans }}
{{ article.title }}{{ article.author.username }}{{ article.createAt|date("d/m/Y") }}{{ article.comments.count }} 47 | 49 | 50 | 51 | 53 | 54 | 55 | 58 | 59 | 60 |
65 |
66 | {% else %} 67 | {{ 'backoffice.articles.list.no_article'|trans }} 68 | {% endif %} 69 |
70 |
71 |
72 | {% endblock %} 73 | 74 | {% block javascripts %} 75 | {{ parent() }} 76 | 77 | {% endblock %} 78 | -------------------------------------------------------------------------------- /templates/backoffice/dashboard/dashboard.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'backoffice/layout-backoffice.html.twig' %} 2 | 3 | {% block body %} 4 |
5 |
6 |
7 |
8 | 9 | 10 | {{ countArticles }} {{ countArticles <= 1 ? 'backoffice.dashboard.article'|trans : 'backoffice.dashboard.articles'|trans }} 11 | 12 |
13 |
14 | 15 |
16 |
17 | 18 | 19 | {{ countComments }} {{ countComments <= 1 ? 'backoffice.dashboard.comment'|trans : 'backoffice.dashboard.comments'|trans }} 20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 | 28 | {{ countUsers }} {{ countUsers <= 1 ? 'backoffice.dashboard.user'|trans : 'backoffice.dashboard.users'|trans }} 29 | 30 |
31 |
32 |
33 |
34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /templates/backoffice/layout-backoffice.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block wrapper %} 4 | {{ parent() }} 5 |
6 | {% include 'backoffice/sidebar/sidebar.html.twig' %} 7 | 8 |
9 | {% block body %}{% endblock %} 10 |
11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /templates/backoffice/sidebar/sidebar.html.twig: -------------------------------------------------------------------------------- 1 | 19 | 20 | -------------------------------------------------------------------------------- /templates/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Symfony-blog{% endblock %} 6 | 7 | {% block stylesheets %} 8 | 9 | {% endblock %} 10 | 11 | 12 | {% block wrapper %} 13 | {% include 'navbar/navbar.html.twig' %} 14 | {% include 'notification-translations/message.html.twig' %} 15 | {% endblock %} 16 | 17 | {% block javascripts %} 18 | {% if is_granted('ROLE_USER') %} 19 | 20 | {% endif %} 21 | 22 | {% endblock %} 23 | 24 | -------------------------------------------------------------------------------- /templates/blog/article/_comment_form.html.twig: -------------------------------------------------------------------------------- 1 | {{ form_start(form, {'action': path('comment_new', {'slug': article.slug}), 'attr': {'id': 'comment-form'} }) }} 2 | {{ form_errors(form) }} 3 |
4 |
5 |

6 | 7 |

8 |
9 |
10 | 11 |
12 | {{ form_label(form.content, 'article.comment'|trans, {'label_attr': {'class': 'label'}}) }} 13 |

14 | {{ form_widget(form.content, {'attr': {'class': 'textarea', 'placeholder': 'article.form.comment_placeholder'|trans}}) }} 15 |

16 |
17 |
18 |

19 | {{ form_rest(form) }} 20 | 21 |

22 |
23 |
24 |
25 | {{ form_end(form) }} 26 | -------------------------------------------------------------------------------- /templates/blog/article/show.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'blog/layout-blog.html.twig' %} 2 | 3 | {% block body %} 4 | {% if article.image is not null %} 5 | {{ article.image.alt }} 6 | {% endif %} 7 | 8 |

{{ article.title }}

9 |

10 | 16 | 17 | 18 |

19 | 20 |
21 |

{{ article.content }}

22 |
23 | 24 |
25 |

26 | {{ article.comments|length }} {{ article.comments|length <= 1 ? 'article.comment'|trans : 'article.comments'|trans }} 27 |

28 | 29 | {% for comment in article.comments|sort|reverse %} 30 |
31 |
32 |

33 | 34 |

35 |
36 |
37 |
38 |

39 | {{ comment.user.username }} 40 |
41 | {{ comment.content }} 42 |

43 |
44 | 56 |
57 |
58 |
59 | {% endfor %} 60 | 61 |
62 |
63 | {% if is_granted('ROLE_USER') %} 64 | {{ render(controller('App\\Controller\\BlogController::commentForm', {'article': article.id})) }} 65 | {% else %} 66 |
67 | {{ 'article.not_authenticate'|trans }}
68 | {{ 'article.login'|trans }} 69 | {{ 'generic.or'|trans }} 70 | {{ 'article.signin'|trans }} 71 |
72 | {% endif %} 73 |
74 |
75 | {% endblock %} 76 | 77 | -------------------------------------------------------------------------------- /templates/blog/home/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'blog/layout-blog.html.twig' %} 2 | 3 | {% block body %} 4 | {% if articles is not empty %} 5 | {% for article in articles %} 6 |
7 |
8 |

9 | {{ article.title }} 10 |

11 |
12 |
13 |
14 | {{ article.content|truncate(50) }} 15 |
16 | 17 | {{ 'generic.by'|trans }} {{ article.author.username }} 18 |
19 |
20 | 25 |
26 | {% endfor %} 27 | 28 | {% include 'paginator/paginator.html.twig' %} 29 | {% else %} 30 |
31 |

{{ 'home.no_article'|trans }}

32 |
33 | {% endif %} 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /templates/blog/layout-blog.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block wrapper %} 4 | {{ parent() }} 5 |
6 |
7 | {% block flashmessage %} 8 | {% include 'flashMessage/flashMessage.html.twig' %} 9 | {% endblock %} 10 | 11 | {% block body %}{% endblock %} 12 | 13 | {% block security %}{% endblock %} 14 |
15 |
16 | 17 | {% block footer %} 18 |
19 |
20 |
21 |

22 | My Symfony blog training to be strong ! Find me on 23 | Github 24 |

25 |

26 | 27 | 28 | 29 |

30 |
31 |
32 |
33 | {% endblock %} 34 | 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /templates/blog/security/login/_login_form.html.twig: -------------------------------------------------------------------------------- 1 | {{ form_start(form, {'attr': {'id': 'login-form'}}) }} 2 | {{ form_errors(form) }} 3 | 4 |
5 | {{ form_label(form.username, 'login.form.username'|trans, {'label_attr': {'class': 'label has-text-left'}}) }} 6 |
7 | {{ form_widget(form.username, {'value': lastUserName, 'attr': {'class': 'input'}}) }} 8 | 9 | 10 | 11 |
12 |
13 | 14 |
15 | {{ form_label(form.password, 'login.form.password'|trans, {'label_attr': {'class': 'label has-text-left'}}) }} 16 |
17 | {{ form_widget(form.password, {'attr': {'class': 'input'}}) }} 18 | 19 | 20 | 21 |
22 |
23 | 24 | {% if (error) %} 25 | 28 | {% endif %} 29 | 30 | 33 | {{ form_end(form) }} 34 | -------------------------------------------------------------------------------- /templates/blog/security/login/login.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'blog/layout-blog.html.twig' %} 2 | 3 | {% block security %} 4 |
5 | 23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/blog/security/password/_password_reset_new_form.html.twig: -------------------------------------------------------------------------------- 1 | {{ form_start(form, {'attr': {'id': 'password-reset-new-form'}}) }} 2 |
3 | {{ form_label(form.plainPassword.first, 'reset_password.form.new_password'|trans, {'label_attr': {'class': 'label has-text-left'}}) }} 4 |
5 | {{ form_widget(form.plainPassword.first, {'attr': {'class': 'input', 'placeholder': 'reset_password.form.new_password_placeholder'|trans}}) }} 6 | 7 | 8 | 9 |
10 |
11 | 12 |
13 | {{ form_label(form.plainPassword.second, 'reset_password.form.repeat_password'|trans, {'label_attr': {'class': 'label has-text-left'}}) }} 14 |
15 | {{ form_widget(form.plainPassword.second, {'attr': {'class': 'input', 'placeholder': 'reset_password.form.repeat_password_placeholder'|trans}}) }} 16 | 17 | 18 | 19 |
20 |
21 | {{ form_errors(form.plainPassword.first) }} 22 |
23 |
24 | 25 | 28 | {{ form_end(form) }} 29 | -------------------------------------------------------------------------------- /templates/blog/security/password/_password_reset_request_form.html.twig: -------------------------------------------------------------------------------- 1 | {{ form_start(form, {'attr': {'id': 'password-reset-request-form'}}) }} 2 |
3 |

4 | {{ 'forgot_password.instruction'|trans }} : 5 |

6 | {{ form_widget(form.email, {'attr': {'class': 'input'}}) }} 7 |
8 | {{ form_errors(form.email) }} 9 | {{ form_errors(form) }} 10 |
11 |
12 | 13 | 16 | {{ form_end(form) }} 17 | -------------------------------------------------------------------------------- /templates/blog/security/password/password_reset_new.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'blog/layout-blog.html.twig' %} 2 | 3 | {% block security %} 4 |
5 |
6 |

{{ 'reset_password.reset_password'|trans }}

7 |
8 | {% include 'blog/security/password/_password_reset_new_form.html.twig' %} 9 |
10 |
11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /templates/blog/security/password/password_reset_request.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'blog/layout-blog.html.twig' %} 2 | 3 | {% block security %} 4 |
5 |
6 |

{{ 'forgot_password.forgot_password'|trans }}

7 |
8 | {% include 'blog/security/password/_password_reset_request_form.html.twig' %} 9 |
10 |
11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /templates/blog/security/registration/_registration_form.html.twig: -------------------------------------------------------------------------------- 1 | {{ form_start(form, {'attr': {'id': 'registration-form'}}) }} 2 | {{ form_errors(form) }} 3 |
4 | {{ form_label(form.username, 'signin.form.username'|trans, {'label_attr': {'class': 'label has-text-left'}}) }} 5 |
6 | {{ form_widget(form.username, {'attr': {'class': 'input', 'placeholder': 'johnDoe'}}) }} 7 | 8 | 9 | 10 |
11 |
12 | {{ form_errors(form.username) }} 13 |
14 |
15 | 16 |
17 | {{ form_label(form.email, 'signin.form.email'|trans, {'label_attr': {'class': 'label has-text-left'}}) }} 18 |
19 | {{ form_widget(form.email, {'attr': {'class': 'input', 'placeholder': 'you@mail.com'}}) }} 20 | 21 | 22 | 23 |
24 |
25 | {{ form_errors(form.email) }} 26 |
27 |
28 | 29 |
30 | {{ form_label(form.plainPassword.first, 'signin.form.password'|trans, {'label_attr': {'class': 'label has-text-left'}}) }} 31 |
32 | {{ form_widget(form.plainPassword.first, {'attr': {'class': 'input', 'placeholder': 'signin.form.password_placeholder'|trans}}) }} 33 | 34 | 35 | 36 |
37 |
38 | 39 |
40 | {{ form_label(form.plainPassword.second, 'signin.form.repeat_password'|trans, {'label_attr': {'class': 'label has-text-left'}}) }} 41 |
42 | {{ form_widget(form.plainPassword.second, {'attr': {'class': 'input', 'placeholder': 'signin.form.repeat_password_placeholder'|trans}}) }} 43 | 44 | 45 | 46 |
47 |
48 | {{ form_errors(form.plainPassword.first) }} 49 |
50 |
51 | 52 | 55 | {{ form_end(form) }} 56 | -------------------------------------------------------------------------------- /templates/blog/security/registration/registration.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'blog/layout-blog.html.twig' %} 2 | 3 | {% block security %} 4 |
5 |
6 |

{{ 'signin.signin'|trans }}

7 |
8 | {% include('blog/security/registration/_registration_form.html.twig') %} 9 |
10 | {{ 'signin.signin_with'|trans }} : 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /templates/blog/user/profile/_sidebar.html.twig: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /templates/blog/user/profile/show.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'blog/layout-blog.html.twig' %} 2 | 3 | {% block body %} 4 |
5 |
6 | {% include('blog/user/profile/_sidebar.html.twig') %} 7 |
8 |
9 |
10 |
11 | 12 |
13 | {{ user.username }} 14 | {{ user.email }} 15 |
16 |
17 |
18 |
19 | {{ 'profile.last_comments'|trans }} 20 |

coming soon

21 |
22 |
23 |
24 | {{ 'profile.badges'|trans }} 25 |

coming soon

26 |
27 |
28 |
29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/email/password_request/_password_reset_email_en.html.twig: -------------------------------------------------------------------------------- 1 |

Hello {{ username }},

2 |

You received this email because a reset password request has been send on your account.
3 | To reset it click on this link below : 4 |

5 |

6 | 7 | {{ url('password_reset_new', {'resetPasswordToken': token}) }} 8 | 9 |

10 | 11 |

If you are not the owner of this request, please ignore it

12 | -------------------------------------------------------------------------------- /templates/email/password_request/_password_reset_email_fr.html.twig: -------------------------------------------------------------------------------- 1 |

Bonjour {{ username }},

2 |

Vous recevez cet email car une demande de réinitialisation de mot de passe a été demandé pour votre compte.
3 | Vous pouvez le réinitialiser en cliquant sur ce lien : 4 |

5 |

6 | 7 | {{ url('password_reset_new', {'resetPasswordToken': token}) }} 8 | 9 |

10 | 11 |

Si vous n'êtes pas à l'origine de cette demande, merci d'ignorer cet email

12 | -------------------------------------------------------------------------------- /templates/flashMessage/flashMessage.html.twig: -------------------------------------------------------------------------------- 1 | {% for flashMessage in app.session.flashbag.get('info') %} 2 |
3 |
4 | 5 | 6 | {{ flashMessage }} 7 |
8 |
9 | {% endfor %} 10 | 11 | {% for flashMessage in app.session.flashbag.get('error') %} 12 |
13 |
14 | 15 | 16 | {{ flashMessage }} 17 |
18 |
19 | {% endfor %} 20 | -------------------------------------------------------------------------------- /templates/navbar/bell-notification.html.twig: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 | Notifications 10 |
11 |
12 | {% if notifications.getNotifications is not empty %} 13 | {% for notification in notifications.getNotifications %} 14 | {% set timer = date('now').diff(date(notification.createdAt)) %} 15 | 16 |
17 | 18 | 19 | {{ notification.createdBy.username }} {{ 'navbar.notification.article_created'|trans }} 20 | 21 |
22 |
23 | {% endfor %} 24 | {% else %} 25 | {{ 'navbar.notification.no-notification'|trans }} 26 | {% endif %} 27 |
28 |
29 | -------------------------------------------------------------------------------- /templates/navbar/navbar.html.twig: -------------------------------------------------------------------------------- 1 | 63 | -------------------------------------------------------------------------------- /templates/notification-translations/message.html.twig: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /templates/paginator/paginator.html.twig: -------------------------------------------------------------------------------- 1 | {% set minLimit = page - 3 %} 2 | {% set maxLimit = page + 3 %} 3 | {% set hellipMin = 0 %} 4 | {% set hellipMax = 0 %} 5 | {% set arrayPages = range(1, nbPages) %} 6 | 7 |
8 | 35 |
36 | -------------------------------------------------------------------------------- /tests/BaseTestCase.php: -------------------------------------------------------------------------------- 1 | selectLink('Log in')->link(); 14 | $crawler = $client->click($link); 15 | 16 | $form = $crawler->selectButton('Log in')->form(); 17 | 18 | $form['login[username]'] = $username; 19 | $form['login[password]'] = $password; 20 | 21 | return $client->submit($form); 22 | } 23 | 24 | public function logout(Client $client) 25 | { 26 | $client->request('GET', '/logout'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Functional/ArticleActionTest.php: -------------------------------------------------------------------------------- 1 | request('GET', '/'); 17 | $crawler = $this->loginAs($client, $crawler, 'admin', 'azerty'); 18 | 19 | $crawler = $client->click($crawler->selectLink('Dashboard')->link()); 20 | $crawler = $client->click($crawler->selectLink('ARTICLES')->link()); 21 | $crawler = $client->click($crawler->selectLink('New article')->link()); 22 | 23 | $form = $crawler->selectButton('Publish')->form(); 24 | 25 | $form['App_article[title]'] = 'An awesome new article'; 26 | $form['App_article[content]'] = 'This is an article wrote by panther'; 27 | $form['app_tag_input'] = 'symfony,panther,'; 28 | $crawler = $client->submit($form); 29 | 30 | // $this->assertEquals('An awesome new article', $crawler->filter('.table > tbody > tr > td')->first()->text()); 31 | 32 | $this->logout($client); 33 | } 34 | 35 | public function testEditAnArticle() 36 | { 37 | $client = static::createPantherClient(); 38 | 39 | $crawler = $client->request('GET', '/'); 40 | 41 | $crawler = $this->loginAs($client, $crawler, 'admin', 'azerty'); 42 | $crawler = $client->click($crawler->selectLink('Dashboard')->link()); 43 | $crawler = $client->click($crawler->selectLink('ARTICLES')->link()); 44 | $crawler = $client->click($crawler->filter('a.is-warning')->eq(3)->link()); 45 | 46 | $form = $crawler->selectButton('Publish')->form(); 47 | 48 | $form['App_article[title]'] = 'Edited by panther'; 49 | 50 | $crawler = $client->submit($form); 51 | 52 | $form = $crawler->selectButton('Publish')->form(); 53 | $formValues = $form->getValues(); 54 | 55 | $this->assertEquals('Edited by panther', $formValues['App_article[title]']); 56 | 57 | $this->logout($client); 58 | } 59 | 60 | // Todo: https://github.com/symfony/panther/issues/203 61 | // public function testDeleteAnArticle() 62 | // { 63 | // $client = static::createPantherClient(); 64 | // 65 | // $crawler = $client->request('GET', '/'); 66 | // 67 | // $crawler = $this->loginAs($client, $crawler, 'admin', 'azerty'); 68 | // 69 | // $crawler = $client->click($crawler->selectLink('Dashboard')->link()); 70 | // $crawler = $client->click($crawler->selectLink('ARTICLES')->link()); 71 | // 72 | // $client->getWebDriver()->switchTo()->alert()->accept(); 73 | // 74 | // $crawler = $client->click($crawler->filter('a.is-danger')->eq(5)->link()); 75 | // 76 | // $client->waitFor('.notification'); 77 | // 78 | // $this->assertContains('The article has been successfully deleted', $crawler->filter('.notification')->text()); 79 | // } 80 | } 81 | -------------------------------------------------------------------------------- /tests/Functional/CommentTest.php: -------------------------------------------------------------------------------- 1 | request('GET', '/'); 14 | 15 | $link = $crawler->selectLink('Read more')->link(); 16 | $crawler = $client->click($link); 17 | 18 | $this->assertContains('You must be login to leave a comment', $crawler->filter('.notification')->text()); 19 | } 20 | 21 | public function testCommentAnArticle() 22 | { 23 | $client = static::createPantherClient(); 24 | 25 | $crawler = $client->request('GET', '/'); 26 | 27 | $crawler = $this->loginAs($client, $crawler, 'johnDoe', 'password'); 28 | 29 | $link = $crawler->selectLink('Read more')->link(); 30 | $crawler = $client->click($link); 31 | 32 | $form = $crawler->selectButton('Leave a comment')->form(); 33 | 34 | $form['App_comment[content]'] = 'Hi ! I am a new comment !'; 35 | $crawler = $client->submit($form); 36 | 37 | $this->assertContains('Hi ! I am a new comment !', $crawler->filter('.content')->text()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Functional/LoginTest.php: -------------------------------------------------------------------------------- 1 | request('GET', '/login'); 14 | $form = $crawler->selectButton('Log in')->form(); 15 | 16 | $form['login[username]'] = 'bob'; 17 | $form['login[password]'] = 'password'; 18 | 19 | $crawler = $client->submit($form); 20 | 21 | $this->assertContains('User or password could not be found', $crawler->filter('.login-message')->text()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Functional/RouteTest.php: -------------------------------------------------------------------------------- 1 | request('GET', '/'); 14 | 15 | $this->assertContains('Training Symfony-blog', $crawler->filter('a')->text()); 16 | } 17 | 18 | public function testLogInPage() 19 | { 20 | $client = static::createPantherClient(); 21 | 22 | $crawler = $client->request('GET', '/login'); 23 | 24 | $this->assertContains('Log in', $crawler->filter('h1')->text()); 25 | } 26 | 27 | public function testSignInPage() 28 | { 29 | $client = static::createPantherClient(); 30 | 31 | $crawler = $client->request('GET', '/registration'); 32 | 33 | $this->assertContains('Sign in', $crawler->filter('h1')->text()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Unit/Form/DataTransformer/TagsTransformerTest.php: -------------------------------------------------------------------------------- 1 | createTag('Symfony'), 17 | $this->createTag('Test'), 18 | $this->createTag('Unit'), 19 | ]; 20 | 21 | $tagsTransformed = $this->getMockedTransformer()->transform($tagsArray); 22 | 23 | $this->assertEquals('Symfony, Test, Unit', $tagsTransformed); 24 | } 25 | 26 | public function testTrimName() 27 | { 28 | $tag = $this->getMockedTransformer()->reverseTransform(' Symfony '); 29 | $this->assertEquals('Symfony', $tag[0]->getName()); 30 | } 31 | 32 | public function testDuplicateTagsName() 33 | { 34 | $tags = $this->getMockedTransformer()->reverseTransform('Hello,Hello,Symfony,Symfony,Test,Symfony'); 35 | $this->assertCount(3, $tags); 36 | } 37 | 38 | public function testAlreadyDefinedTags() 39 | { 40 | $tagArray = [ 41 | $this->createTag('Symfony'), 42 | $this->createTag('Unit'), 43 | ]; 44 | 45 | $tags = $this->getMockedTransformer($tagArray)->reverseTransform('Symfony,Unit,Feature,Docker'); 46 | $this->assertCount(4, $tags); 47 | $this->assertSame($tagArray[0], $tags[0]); 48 | $this->assertSame($tagArray[1], $tags[1]); 49 | } 50 | 51 | public function testRemoveEmptyTags() 52 | { 53 | $tags = $this->getMockedTransformer()->reverseTransform('Unit, , , ,,Symfony'); 54 | $this->assertCount(2, $tags); 55 | $this->assertEquals('Symfony', $tags[1]); 56 | } 57 | 58 | private function getMockedTransformer(array $findByReturnValues = []): TagsTransformer 59 | { 60 | $tagRepository = $this 61 | ->getMockBuilder(EntityRepository::class) 62 | ->disableOriginalConstructor() 63 | ->getMock(); 64 | $tagRepository->expects($this->any()) 65 | ->method('findBy') 66 | ->willReturn($findByReturnValues); 67 | 68 | $entityManager = $this 69 | ->getMockBuilder(ObjectManager::class) 70 | ->disableOriginalConstructor() 71 | ->getMock(); 72 | $entityManager->expects($this->any()) 73 | ->method('getRepository') 74 | ->willReturn($tagRepository); 75 | 76 | return new TagsTransformer($entityManager); 77 | } 78 | 79 | private function createTag($name): Tag 80 | { 81 | $tag = new Tag(); 82 | $tag->setName($name); 83 | 84 | return $tag; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/Unit/Services/PaginatorTest.php: -------------------------------------------------------------------------------- 1 | createArticle(), 16 | $this->createArticle(), 17 | $this->createArticle(), 18 | $this->createArticle(), 19 | $this->createArticle(), 20 | $this->createArticle(), 21 | ]; 22 | 23 | $this->assertEquals($this->getMockedPaginator()->countPage($articleArray), '2'); 24 | } 25 | 26 | private function getMockedPaginator() 27 | { 28 | $requestStack = $this 29 | ->getMockBuilder(RequestStack::class) 30 | ->disableOriginalConstructor() 31 | ->getMock(); 32 | 33 | return new Paginator($requestStack, '5'); 34 | } 35 | 36 | private function createArticle() 37 | { 38 | $article = new Article(); 39 | 40 | return $article; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Unit/Services/TokenPasswordTest.php: -------------------------------------------------------------------------------- 1 | getEntityManagerMock(); 16 | $em 17 | ->expects($this->once()) 18 | ->method('flush') 19 | ; 20 | $tokenGenerator = $this->getTokenGeneratorMock(); 21 | $tokenGenerator 22 | ->expects($this->any()) 23 | ->method('generateToken') 24 | ->willReturn('token-genereted') 25 | ; 26 | 27 | $user = new User(); 28 | 29 | $resetPassword = new TokenPassword($em, $tokenGenerator); 30 | $resetPassword->addToken($user); 31 | 32 | $this->assertEquals('token-genereted', $user->getResetPasswordToken()); 33 | $this->assertNotNull($user->getTokenExpirationDate()); 34 | } 35 | 36 | private function getEntityManagerMock() 37 | { 38 | return $this 39 | ->getMockBuilder(EntityManagerInterface::class) 40 | ->disableOriginalConstructor() 41 | ->getMock(); 42 | } 43 | 44 | private function getTokenGeneratorMock() 45 | { 46 | return $this 47 | ->getMockBuilder(TokenGeneratorInterface::class) 48 | ->disableOriginalConstructor() 49 | ->getMock(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Unit/Services/UploaderTest.php: -------------------------------------------------------------------------------- 1 | uploader = new Uploader('%kernel.project_dir%/public/uploads/articles/coverages/'); 20 | } 21 | 22 | public function testHasNotNewImage() 23 | { 24 | $image = $this->getImage(); 25 | $this->assertFalse($this->uploader->hasNewImage($image)); 26 | } 27 | 28 | public function testHasNewImage() 29 | { 30 | $image = $this->getImage(); 31 | $image->setFile($this->mockedFile('test.png')); 32 | $this->assertTrue($this->uploader->hasNewImage($image)); 33 | } 34 | 35 | public function testHasNoActiveImage() 36 | { 37 | $image = $this->getImage(); 38 | $this->assertFalse($this->uploader->hasActiveImage($image)); 39 | } 40 | 41 | public function testHasActiveImage() 42 | { 43 | $image = $this 44 | ->getMockBuilder(Image::class) 45 | ->disableOriginalConstructor() 46 | ->getMock(); 47 | $image 48 | ->method('getId') 49 | ->willReturn(384); 50 | 51 | $this->assertTrue($this->uploader->hasActiveImage($image)); 52 | } 53 | 54 | public function testDeletedImageIsChecked() 55 | { 56 | $image = $this->getImage(); 57 | $image->setDeletedImage(true); 58 | 59 | $this->assertTrue($this->uploader->isDeleteImageChecked($image)); 60 | } 61 | 62 | public function testDeletedImageIsNotChecked() 63 | { 64 | $image = $this->getImage(); 65 | $image->setDeletedImage(false); 66 | 67 | $this->assertFalse($this->uploader->isDeleteImageChecked($image)); 68 | } 69 | 70 | public function testUploadImage() 71 | { 72 | $uploadedFile = $this 73 | ->getMockBuilder(UploadedFile::class) 74 | ->disableOriginalConstructor() 75 | ->getMock(); 76 | $uploadedFile 77 | ->expects($this->once()) 78 | ->method('move'); 79 | 80 | $this->uploader->uploadImage($uploadedFile, 'sweet_name'); 81 | } 82 | 83 | private function mockedFile(string $fileName): UploadedFile 84 | { 85 | return new UploadedFile(__FILE__, $fileName); 86 | } 87 | 88 | private function getImage(): Image 89 | { 90 | return new Image(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__).'/.env'); 11 | } 12 | -------------------------------------------------------------------------------- /translations/emails.en.yaml: -------------------------------------------------------------------------------- 1 | reset_password: 2 | title: Reset password request 3 | -------------------------------------------------------------------------------- /translations/emails.fr.yaml: -------------------------------------------------------------------------------- 1 | reset_password: 2 | title: Demande reinitialisation de mot de passe 3 | -------------------------------------------------------------------------------- /translations/messages.en.yaml: -------------------------------------------------------------------------------- 1 | generic: 2 | or: or 3 | by: by 4 | 5 | navbar: 6 | login: Log in 7 | signin: Sign in 8 | profile: Profile 9 | logout: Logout 10 | dashboard: Dashboard 11 | notification: 12 | no-notification: No notification 13 | article_created: published a new article 14 | 15 | home: 16 | read_more: Read more 17 | no_article: No article :( 18 | 19 | article: 20 | comment: Comment 21 | comments: Comments 22 | not_authenticate: You must be login to leave a comment 23 | login: Log in 24 | signin: Sign in 25 | form: 26 | post_comment: Leave a comment 27 | comment_placeholder: Leave a public comment.. 28 | 29 | login: 30 | login: Log in 31 | login_with: Log in with 32 | forgot_password: Forgot password ? 33 | not_registered: Not registered ? 34 | create_account: Create an account 35 | form: 36 | username: Username 37 | password: Password 38 | submitted: Log in... 39 | flashmessage_success: You are now log in 40 | 41 | 42 | "Username could not be found.": User or password could not be found 43 | 44 | signin: 45 | signin: Sign in 46 | signin_with: Sign in with 47 | form: 48 | username: Username 49 | email: Email 50 | password: Password 51 | password_placeholder: strong password 52 | repeat_password: Repeat it 53 | repeat_password_placeholder: once again 54 | submitted: Sign in... 55 | flashmessage_success: Your account has been created. You can now log in 56 | 57 | 58 | forgot_password: 59 | forgot_password: Forgot password 60 | instruction: Fill in the field with you email account to get a reset link 61 | send: Send email 62 | submitted: Send... 63 | flashmessage_success: An email has been sent to this email address 64 | 65 | reset_password: 66 | reset_password: Reset password 67 | form: 68 | new_password: New password 69 | new_password_placeholder: strong password 70 | repeat_password: Repeat the new password 71 | repeat_password_placeholder: once again 72 | submit: Save 73 | submitted: Save in progress... 74 | flashmessage_success: Password has been reseted ! 75 | flashmessage_token_expired: Token is expired. Get a new password request 76 | 77 | profile: 78 | sidebar: 79 | profile: Profile 80 | security: Security 81 | my_profile: My profile 82 | edit_profile: Edit my profile 83 | change_password: Change my password 84 | last_comments: Last comments 85 | badges: My badges 86 | 87 | backoffice: 88 | sidebar: 89 | dashboard: DASHBOARD 90 | articles: ARTICLES 91 | dashboard: 92 | article: Article 93 | articles: Articles 94 | comment: Comment 95 | comments: Comments 96 | user: User 97 | users: Users 98 | articles: 99 | list: 100 | title: THE ARTICLES 101 | new_article: New article 102 | article_title: Title 103 | article_author: Author 104 | article_posted_at: Posted at 105 | article_comment: Comments 106 | articlr_action: Actions 107 | no_article: No article 108 | deleted_confirm: Do you want to delete this article ? 109 | add: 110 | title: ADD ARTICLE 111 | edit: 112 | title: EDIT ARTICLE 113 | cover_image: Cover image 114 | delete_cover_image: Delete image 115 | delete_cover_image_confirm: Do you want to delete this image ? 116 | no_cover_image: No cover image 117 | add_edit_form: 118 | actuality: Actuality 119 | article_title: Title 120 | article_content: Content 121 | article_tags: Tags 122 | article_tags_helper: Split your tags with a comma 123 | article_media: Media 124 | submit_button: Publish 125 | flashmessage_publish: The article has been successfully created 126 | flashmessage_edit: The article has been successfully edited 127 | flashmessage_deleted_article: The article has been successfully deleted 128 | 129 | -------------------------------------------------------------------------------- /translations/validators.en.yaml: -------------------------------------------------------------------------------- 1 | signin: 2 | unique_username: username is already used 3 | required_username: username is required 4 | min_char_username: username must have 3 characters at least 5 | wrong_char_username: username has wrong characters 6 | unique_email: email is already used 7 | required_email: email is required 8 | wrong_format_email: email format must be valid 9 | required_password: password is required 10 | min_char_password: password must have 6 characters at least 11 | same_password: passwords must be the same 12 | 13 | forgot_password: 14 | email_required: email is required 15 | email_not_exist: email doesn't exist 16 | 17 | reset_password: 18 | same_password: passwords must be the same 19 | 20 | backoffice: 21 | article: 22 | title_required: title is required 23 | content_required: content is required 24 | image_extension: file is not a valid image 25 | 26 | -------------------------------------------------------------------------------- /translations/validators.fr.yaml: -------------------------------------------------------------------------------- 1 | signin: 2 | unique_username: l'identifiant est déjà utilisé 3 | required_username: l'identifiant est obligatoire 4 | min_char_username: l'identifiant doit contenir 3 caractères minimum 5 | wrong_char_username: l'identifiant contient des caractères interdits 6 | unique_email: l'email est déjà utilisé 7 | required_email: l'email est obligatoire 8 | wrong_format_email: le format de l'email n'est pas valide 9 | required_password: Le mot de passe est obligatoire 10 | min_char_password: Le mot de passe doit contenir 6 caractères minimum 11 | same_password: les mots de passe doivent être identiques 12 | 13 | forgot_password: 14 | email_required: l'email est obligatoire 15 | email_not_exist: L'email renseigné n'est lié à aucun compte 16 | 17 | reset_password: 18 | same_password: Les mots de passe doivent être identiques 19 | 20 | backoffice: 21 | article: 22 | title_required: le titre est obligatoire 23 | content_required: le contenu est obligatoire 24 | image_extension: le fichier n'est pas une image valide 25 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var Encore = require('@symfony/webpack-encore'); 2 | 3 | Encore 4 | // directory where compiled assets will be stored 5 | .setOutputPath('public/build/') 6 | // public path used by the web server to access the output path 7 | .setPublicPath('/build') 8 | // only needed for CDN's or sub-directory deploy 9 | //.setManifestKeyPrefix('build/') 10 | 11 | /* 12 | * ENTRY CONFIG 13 | * 14 | * Add 1 entry for each "page" of your app 15 | * (including one that's included on every page - e.g. "app") 16 | * 17 | * Each entry will result in one JavaScript file (e.g. app.js) 18 | * and one CSS file (e.g. app.css) if you JavaScript imports CSS. 19 | */ 20 | .addEntry('/js/app', './assets/js/app.js') 21 | .addEntry('/js/delete-confirmation', './assets/js/delete-confirmation.js') 22 | .addEntry('/js/mercure-subscribe', './assets/js/mercure-subscribe.js') 23 | .addEntry('/js/tags', './assets/js/tags.js') 24 | .addStyleEntry('/css/app', ['./assets/scss/app.scss']) 25 | 26 | .enableBuildNotifications() 27 | .enableSassLoader() 28 | ; 29 | 30 | module.exports = Encore.getWebpackConfig(); 31 | --------------------------------------------------------------------------------