install symfony demo with installer

This commit is contained in:
Tchama 2021-04-16 09:37:45 +02:00
parent 72fd1cf02d
commit 3751003188
191 changed files with 32965 additions and 1 deletions

2
.gitignore vendored
View File

@ -1,5 +1,5 @@
# Comment to include Symfony project
app/*
#app/*
!app/.keep
# Ignore Database datas

10
app/.editorconfig Normal file
View File

@ -0,0 +1,10 @@
; top-most EditorConfig file
root = true
; Unix-style newlines
[*]
end_of_line = LF
[*.php]
indent_style = space
indent_size = 4

34
app/.env Normal file
View File

@ -0,0 +1,34 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=2ca64f8d83b9e89f5f19d672841d6bb8
#TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
#TRUSTED_HOSTS='^(localhost|example\.com)$'
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# For a MySQL database, use: "mysql://db_user:db_password@127.0.0.1:3306/db_name"
# For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8"
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#DATABASE_URL=sqlite:///%kernel.project_dir%/data/database.sqlite
DATABASE_URL="postgresql://postgres:secret@db:5432/postgres?serverVersion=12&charset=utf8"
###< doctrine/doctrine-bundle ###
###> symfony/mailer ###
# MAILER_DSN=smtp://localhost
###< symfony/mailer ###

6
app/.env.test Normal file
View File

@ -0,0 +1,6 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
DATABASE_URL=sqlite:///%kernel.project_dir%/data/database_test.sqlite

80
app/.github/workflows/ci.yaml vendored Normal file
View File

@ -0,0 +1,80 @@
name: "CI"
on:
pull_request:
push:
branches:
- 'master'
env:
fail-fast: true
PHPUNIT_FLAGS: "-v"
SYMFONY_PHPUNIT_DIR: "$HOME/symfony-bridge/.phpunit"
SYMFONY_REQUIRE: ">=4.4"
# 40x: Since symfony/monolog-bridge 5.2:
# Passing an actionLevel (int|string) as constructor's 3rd argument of
# "Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy"
# is deprecated, "Monolog\Handler\FingersCrossed\ActivationStrategyInterface" expected.
SYMFONY_DEPRECATIONS_HELPER: 40
jobs:
test:
name: "${{ matrix.operating-system }} / PHP ${{ matrix.php-version }}"
runs-on: ${{ matrix.operating-system }}
continue-on-error: false
strategy:
matrix:
operating-system: ['ubuntu-latest', 'windows-latest', 'macos-latest']
php-version: ['7.2.9', '7.3', '7.4', '8.0']
steps:
- name: "Checkout code"
uses: actions/checkout@v2.3.3
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@2.7.0
with:
coverage: "none"
extensions: "intl, mbstring, pdo_sqlite"
php-version: ${{ matrix.php-version }}
tools: composer:v2
- name: "Add PHPUnit matcher"
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: "Set composer cache directory"
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: "Cache composer"
uses: actions/cache@v2.1.2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('composer.json') }}
restore-keys: ${{ runner.os }}-${{ matrix.php-version }}-composer-
- name: "Require symfony/flex"
run: composer global require --no-progress --no-scripts --no-plugins symfony/flex dev-main
- if: matrix.php-version != '8.0'
run: composer update
- if: matrix.php-version == '8.0'
run: composer update --ignore-platform-req=php
- if: matrix.php-version != '8.0'
name: "Install PHPUnit"
run: vendor/bin/simple-phpunit install
- if: matrix.php-version == '8.0'
name: "Install PHPUnit for PHP 8"
run: |
echo 'SYMFONY_PHPUNIT_VERSION=9.4' >> $GITHUB_ENV
vendor/bin/simple-phpunit install
- name: "PHPUnit version"
run: vendor/bin/simple-phpunit --version
- name: "Run tests"
run: vendor/bin/simple-phpunit ${{ env.PHPUNIT_FLAGS }}

88
app/.github/workflows/lint.yaml vendored Normal file
View File

@ -0,0 +1,88 @@
name: "Lint"
on: [push, pull_request]
env:
fail-fast: true
jobs:
php-cs-fixer:
name: PHP-CS-Fixer
runs-on: ubuntu-latest
steps:
- name: "Checkout code"
uses: actions/checkout@v2
- name: PHP-CS-Fixer
uses: docker://oskarstark/php-cs-fixer-ga
with:
args: --diff --dry-run
linters:
name: Linters
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['7.4']
steps:
- name: "Checkout code"
uses: actions/checkout@v2.3.3
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@2.7.0
with:
coverage: "none"
extensions: intl
php-version: ${{ matrix.php-version }}
tools: composer:v2
- name: "Set composer cache directory"
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: "Cache composer"
uses: actions/cache@v2.1.2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('composer.json') }}
restore-keys: ${{ runner.os }}-${{ matrix.php-version }}-composer-
- name: "Require symfony/flex"
run: composer global require --no-progress --no-scripts --no-plugins symfony/flex dev-main
- name: "Composer update"
id: install
run: composer update --no-scripts
- name: Lint YAML files
if: always() && steps.install.outcome == 'success'
run: ./bin/console lint:yaml config --parse-tags
- name: Lint Twig templates
if: always() && steps.install.outcome == 'success'
run: ./bin/console lint:twig templates --env=prod
- name: Lint XLIFF translations
if: always() && steps.install.outcome == 'success'
run: ./bin/console lint:xliff translations
- name: Lint Parameters and Services
if: always() && steps.install.outcome == 'success'
run: ./bin/console lint:container
- name: Lint Doctrine entities
if: always() && steps.install.outcome == 'success'
run: ./bin/console doctrine:schema:validate --skip-sync -vvv --no-interaction
- name: Lint Composer config
if: always() && steps.install.outcome == 'success'
run: composer validate --strict
- name: Download Symfony CLI
if: always() && steps.install.outcome == 'success'
run: wget https://get.symfony.com/cli/installer -O - | bash
- name: Check if any dependencies are compromised
if: always() && steps.install.outcome == 'success'
run: /home/runner/.symfony/bin/symfony check:security

24
app/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
/public/build/fonts/glyphicons-*
/public/build/images/glyphicons-*
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
/vendor/
###< symfony/framework-bundle ###
###> symfony/phpunit-bridge ###
.phpunit
.phpunit.result.cache
/phpunit.xml
###< symfony/phpunit-bridge ###
###> symfony/webpack-encore-bundle ###
/node_modules/
/public/build/
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###

42
app/.php_cs.dist Normal file
View File

@ -0,0 +1,42 @@
<?php
$fileHeaderComment = <<<COMMENT
This file is part of the Symfony package.
(c) Fabien Potencier <fabien@symfony.com>
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
COMMENT;
$finder = PhpCsFixer\Finder::create()
->in(__DIR__)
->exclude('config')
->exclude('var')
->exclude('public/bundles')
->exclude('public/build')
// exclude files generated by Symfony Flex recipes
->notPath('bin/console')
->notPath('public/index.php')
;
return (new PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@Symfony' => true,
'@Symfony:risky' => true,
'header_comment' => ['header' => $fileHeaderComment, 'separate' => 'both'],
'linebreak_after_opening_tag' => true,
'mb_str_functions' => true,
'no_php4_constructor' => true,
'no_unreachable_default_argument_value' => true,
'no_useless_else' => true,
'no_useless_return' => true,
'php_unit_strict' => true,
'phpdoc_order' => true,
'strict_comparison' => true,
'strict_param' => true,
])
->setFinder($finder)
->setCacheFile(__DIR__.'/var/.php_cs.cache')
;

7
app/CONTRIBUTING.md Normal file
View File

@ -0,0 +1,7 @@
Contributing
============
The Symfony Demo application is an open source project. Contributions made by
the community are welcome. Send us your ideas, code reviews, pull requests and
feature requests to help us improve this project. All contributions must follow
the [usual Symfony contribution requirements](https://symfony.com/doc/current/contributing/index.html).

60
app/README.md Normal file
View File

@ -0,0 +1,60 @@
Symfony Demo Application
========================
The "Symfony Demo Application" is a reference application created to show how
to develop applications following the [Symfony Best Practices][1].
Requirements
------------
* PHP 7.2.9 or higher;
* PDO-SQLite PHP extension enabled;
* and the [usual Symfony application requirements][2].
Installation
------------
[Download Symfony][4] to install the `symfony` binary on your computer and run
this command:
```bash
$ symfony new --demo my_project
```
Alternatively, you can use Composer:
```bash
$ composer create-project symfony/symfony-demo my_project
```
Usage
-----
There's no need to configure anything to run the application. If you have
[installed Symfony][4] binary, run this command:
```bash
$ cd my_project/
$ symfony serve
```
Then access the application in your browser at the given URL (<https://localhost:8000> by default).
If you don't have the Symfony binary installed, run `php -S localhost:8000 -t public/`
to use the built-in PHP web server or [configure a web server][3] like Nginx or
Apache to run the application.
Tests
-----
Execute this command to run tests:
```bash
$ cd my_project/
$ ./bin/phpunit
```
[1]: https://symfony.com/doc/current/best_practices.html
[2]: https://symfony.com/doc/current/reference/requirements.html
[3]: https://symfony.com/doc/current/cookbook/configuration/web_server_configuration.html
[4]: https://symfony.com/download

64
app/assets/js/admin.js Normal file
View File

@ -0,0 +1,64 @@
import '../scss/admin.scss';
import 'eonasdan-bootstrap-datetimepicker';
import 'typeahead.js';
import Bloodhound from "bloodhound-js";
import 'bootstrap-tagsinput';
$(function() {
// Datetime picker initialization.
// See https://eonasdan.github.io/bootstrap-datetimepicker/
$('[data-toggle="datetimepicker"]').datetimepicker({
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-check-circle-o',
clear: 'fa fa-trash',
close: 'fa fa-remove'
}
});
// Bootstrap-tagsinput initialization
// https://bootstrap-tagsinput.github.io/bootstrap-tagsinput/examples/
var $input = $('input[data-toggle="tagsinput"]');
if ($input.length) {
var source = new Bloodhound({
local: $input.data('tags'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
datumTokenizer: Bloodhound.tokenizers.whitespace
});
source.initialize();
$input.tagsinput({
trimValue: true,
focusClass: 'focus',
typeaheadjs: {
name: 'tags',
source: source.ttAdapter()
}
});
}
});
// Handling the modal confirmation message.
$(document).on('submit', 'form[data-confirmation]', function (event) {
var $form = $(this),
$confirm = $('#confirmationModal');
if ($confirm.data('result') !== 'yes') {
//cancel submit event
event.preventDefault();
$confirm
.off('click', '#btnYes')
.on('click', '#btnYes', function () {
$confirm.data('result', 'yes');
$form.find('input[type="submit"]').attr('disabled', 'disabled');
$form.submit();
})
.modal('show');
}
});

15
app/assets/js/app.js Normal file
View File

@ -0,0 +1,15 @@
import '../scss/app.scss';
// loads the Bootstrap jQuery plugins
import 'bootstrap-sass/assets/javascripts/bootstrap/transition.js';
import 'bootstrap-sass/assets/javascripts/bootstrap/alert.js';
import 'bootstrap-sass/assets/javascripts/bootstrap/collapse.js';
import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown.js';
import 'bootstrap-sass/assets/javascripts/bootstrap/modal.js';
import 'jquery'
// loads the code syntax highlighting library
import './highlight.js';
// Creates links to the Symfony documentation
import './doclinks.js';

58
app/assets/js/doclinks.js Normal file
View File

@ -0,0 +1,58 @@
'use strict';
// Wraps some elements in anchor tags referencing to the Symfony documentation
$(function() {
var $modal = $('#sourceCodeModal');
var $controllerCode = $modal.find('code.php');
var $templateCode = $modal.find('code.twig');
function anchor(url, content) {
return '<a class="doclink" target="_blank" href="' + url + '">' + content + '</a>';
};
// Wraps links to the Symfony documentation
$modal.find('.hljs-comment').each(function() {
$(this).html($(this).html().replace(/https:\/\/symfony.com\/doc\/[\w/.#-]+/g, function(url) {
return anchor(url, url);
}));
});
// Wraps Symfony's annotations
var annotations = {
'@Cache': 'https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/cache.html',
'@IsGranted': 'https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/security.html#isgranted',
'@ParamConverter': 'https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html',
'@Route': 'https://symfony.com/doc/current/routing.html#creating-routes-as-annotations',
'@Security': 'https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/security.html#security'
};
$controllerCode.find('.hljs-doctag').each(function() {
var annotation = $(this).text();
if (annotations[annotation]) {
$(this).html(anchor(annotations[annotation], annotation));
}
});
// Wraps Twig's tags
$templateCode.find('.hljs-template-tag > .hljs-name').each(function() {
var tag = $(this).text();
if ('else' === tag || tag.match(/^end/)) {
return;
}
var url = 'https://twig.symfony.com/doc/3.x/tags/' + tag + '.html#' + tag;
$(this).html(anchor(url, tag));
});
// Wraps Twig's functions
$templateCode.find('.hljs-template-variable > .hljs-name').each(function() {
var func = $(this).text();
var url = 'https://twig.symfony.com/doc/3.x/functions/' + func + '.html#' + func;
$(this).html(anchor(url, func));
});
});

View File

@ -0,0 +1,8 @@
import hljs from 'highlight.js/lib/highlight';
import php from 'highlight.js/lib/languages/php';
import twig from 'highlight.js/lib/languages/twig';
hljs.registerLanguage('php', php);
hljs.registerLanguage('twig', twig);
hljs.initHighlightingOnLoad();

View File

@ -0,0 +1,106 @@
/**
* jQuery plugin for an instant searching.
*
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
(function ($) {
'use strict';
String.prototype.render = function (parameters) {
return this.replace(/({{ (\w+) }})/g, function (match, pattern, name) {
return parameters[name];
})
};
// INSTANTS SEARCH PUBLIC CLASS DEFINITION
// =======================================
var InstantSearch = function (element, options) {
this.$input = $(element);
this.$form = this.$input.closest('form');
this.$preview = $('<ul class="search-preview list-group">').appendTo(this.$form);
this.options = $.extend({}, InstantSearch.DEFAULTS, this.$input.data(), options);
this.$input.keyup(this.debounce());
};
InstantSearch.DEFAULTS = {
minQueryLength: 2,
limit: 10,
delay: 500,
noResultsMessage: 'No results found',
itemTemplate: '\
<article class="post">\
<h2><a href="{{ url }}">{{ title }}</a></h2>\
<p class="post-metadata">\
<span class="metadata"><i class="fa fa-calendar"></i> {{ date }}</span>\
<span class="metadata"><i class="fa fa-user"></i> {{ author }}</span>\
</p>\
<p>{{ summary }}</p>\
</article>'
};
InstantSearch.prototype.debounce = function () {
var delay = this.options.delay;
var search = this.search;
var timer = null;
var self = this;
return function () {
clearTimeout(timer);
timer = setTimeout(function () {
search.apply(self);
}, delay);
};
};
InstantSearch.prototype.search = function () {
var query = $.trim(this.$input.val()).replace(/\s{2,}/g, ' ');
if (query.length < this.options.minQueryLength) {
this.$preview.empty();
return;
}
var self = this;
var data = this.$form.serializeArray();
data['l'] = this.limit;
$.getJSON(this.$form.attr('action'), data, function (items) {
self.show(items);
});
};
InstantSearch.prototype.show = function (items) {
var $preview = this.$preview;
var itemTemplate = this.options.itemTemplate;
if (0 === items.length) {
$preview.html(this.options.noResultsMessage);
} else {
$preview.empty();
$.each(items, function (index, item) {
$preview.append(itemTemplate.render(item));
});
}
};
// INSTANTS SEARCH PLUGIN DEFINITION
// =================================
function Plugin(option) {
return this.each(function () {
var $this = $(this);
var instance = $this.data('instantSearch');
var options = typeof option === 'object' && option;
if (!instance) $this.data('instantSearch', (instance = new InstantSearch(this, options)));
if (option === 'search') instance.search();
})
}
$.fn.instantSearch = Plugin;
$.fn.instantSearch.Constructor = InstantSearch;
})(window.jQuery);

11
app/assets/js/login.js Normal file
View File

@ -0,0 +1,11 @@
$(function() {
var usernameEl = $('#username');
var passwordEl = $('#password');
// in a real application, the user/password should never be hardcoded
// but for the demo application it's very convenient to do so
if (!usernameEl.val() || 'jane_admin' === usernameEl.val()) {
usernameEl.val('jane_admin');
passwordEl.val('kitten');
}
});

9
app/assets/js/search.js Normal file
View File

@ -0,0 +1,9 @@
import './jquery.instantSearch.js';
$(function() {
$('.search-field')
.instantSearch({
delay: 100,
})
.keyup();
});

View File

@ -0,0 +1,26 @@
@import "~bootswatch/flatly/variables";
@import "~eonasdan-bootstrap-datetimepicker/src/sass/bootstrap-datetimepicker-build.scss";
@import "bootstrap-tagsinput.scss";
/* Page: 'Backend post index'
------------------------------------------------------------------------- */
body#admin_post_index .item-actions {
white-space: nowrap
}
body#admin_post_index .item-actions a.btn + a.btn {
margin-left: 4px
}
/* Page: 'Backend post show'
------------------------------------------------------------------------- */
body#admin_post_show .post-tags .label-default {
background-color: #e9ecec;
color: #6D8283;
font-size: 16px;
margin-right: 10px;
padding: .4em 1em .5em;
}
body#admin_post_show .post-tags .label-default i {
color: #95A6A7;
}

360
app/assets/scss/app.scss Normal file
View File

@ -0,0 +1,360 @@
// setting the value of this variable to an empty data URL is the only working solution
// to load the Bootswatch web fonts locally and avoid loading them from Google servers
// see https://github.com/thomaspark/bootswatch/issues/55#issuecomment-298093182
$web-font-path: 'data:text/css;base64,';
// Make sure the bootstrap-sass and lato fonts are resolved correctly
$icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/";
$lato-font-path: '~lato-font/fonts';
@import "~bootswatch/flatly/variables";
@import "~bootstrap-sass/assets/stylesheets/bootstrap";
@import "~bootswatch/flatly/bootswatch";
@import "~@fortawesome/fontawesome-free/css/all.css";
@import "~@fortawesome/fontawesome-free/css/v4-shims.css";
@import "~highlight.js/styles/solarized-light.css";
// pick the Lato fonts individually to avoid importing the entire font family
@import '~lato-font/scss/public-api';
@include lato-include-font('normal');
@include lato-include-font('bold');
/* Basic styles
------------------------------------------------------------------------- */
p, ul, ol {
font-size: 19px;
margin-bottom: 1.5em
}
li {
margin-bottom: 0.5em
}
code {
background: #ecf0f1;
color: #2c3e50
}
.text-danger, .text-danger:hover {
color: #e74c3c
}
i {
margin-right: 0.25em
}
.table.table-middle-aligned th,
.table.table-middle-aligned td {
vertical-align: middle;
}
.doclink {
color: inherit
}
/* Utilities
------------------------------------------------------------------------- */
.m-b-0 { margin-bottom: 0 }
/* Page elements
------------------------------------------------------------------------- */
body {
display: flex;
flex-direction: column;
min-height: 100vh
}
header {
margin-bottom: 2em
}
header ul.nav li {
margin-bottom: 0
}
header .locales {
min-width: 190px;
}
header .locales a {
color: #212529;
padding: 3px 15px;
}
header .locales a small {
border-radius: 4px;
border: 2px solid #dee2e6;
color: #7b8a8b;
float: left;
font-size: 12px;
line-height: 1.1;
margin: 2px 10px 0 0;
min-width: 26px;
padding: 0px 3px;
text-align: center;
text-transform: uppercase;
}
header .locales .active small,
header .locales a:hover small {
color: inherit;
}
.body-container {
flex: 1;
/* needed to prevent pages with a very small height and browsers not supporting flex */
min-height: 600px
}
.body-container #main h1, .body-container #main h2 {
margin-top: 0
}
#sidebar .section {
margin-bottom: 2em
}
#sidebar p {
font-size: 15px
}
#sidebar p + p {
margin: 1.5em 0 0
}
footer {
background: #ecf0f1;
margin-top: 2em;
padding-top: 2em;
padding-bottom: 2em
}
footer p {
color: #7b8a8b;
font-size: 13px;
margin-bottom: 0.25em
}
footer #footer-resources {
text-align: right
}
footer #footer-resources i {
color: #7b8a8b;
font-size: 28.5px;
margin-left: 0.5em
}
#sourceCodeModal h3 {
font-size: 19px;
margin-top: 0
}
#sourceCodeModal h3 small {
color: #7b8a8b;
font-size: 80%
}
#sourceCodeModal pre {
margin-bottom: 2em;
padding: 0
}
#confirmationModal .modal-dialog {
width: 500px
}
#confirmationModal .modal-footer button {
min-width: 75px
}
/* Misc. elements
------------------------------------------------------------------------- */
.section.rss a {
color: #f39c12;
font-size: 21px;
}
/* Forms
------------------------------------------------------------------------- */
.form-group.has-error .form-control {
border-color: #e74c3c
}
.form-group.has-error .control-label {
color: #e74c3c
}
.form-group.has-error .help-block {
background-color: #e74c3c;
color: #fff;
font-size: 15px;
padding: 1em
}
.form-group.has-error .help-block ul,
.form-group.has-error .help-block li {
margin-bottom: 0
}
.form-group.has-error .help-block li + li {
margin-top: 0.5em;
}
textarea {
max-width: 100%
}
/* Page: 'Technical Requirements Checker'
------------------------------------------------------------------------- */
body#requirements_checker header h1 {
margin-bottom: 0;
margin-top: 0
}
body#requirements_checker header h1 span {
font-size: 120%;
opacity: 0.7;
padding: 0 5px
}
body#requirements_checker .panel li {
margin-bottom: 1em
}
/* Page: 'Homepage'
------------------------------------------------------------------------- */
body#homepage {
text-align: center
}
/* Page: 'Login'
------------------------------------------------------------------------- */
body#login #login-users-help p {
font-size: 15px;
line-height: 1.42857
}
body#login #login-users-help p:last-child {
margin-bottom: 0
}
body#login #login-users-help p .label {
margin-right: 5px
}
body#login #login-users-help p .console {
display: block;
margin: 5px 0;
padding: 10px
}
/* Common Blog page elements
------------------------------------------------------------------------- */
.post-metadata {
color: #b4bcc2;
font-size: 19px;
margin-bottom: 16px;
}
.post-metadata .metadata {
margin-right: 1.5em;
}
.post-tags .label {
margin-right: 5px;
}
/* Page: 'Blog index'
------------------------------------------------------------------------- */
body#blog_index #main h1,
body#blog_index #main p {
margin-bottom: 0.5em
}
body#blog_index article.post {
margin-bottom: 3em;
}
body#blog_index .post-metadata {
font-size: 16px;
margin-bottom: 8px;
}
body#blog_index .post-tags .label-default {
background-color: #e9ecec;
color: #6d8283;
}
body#blog_index .post-tags .label-default i {
color: #a3b2b2;
}
/* Page: 'Blog post show'
------------------------------------------------------------------------- */
body#blog_post_show #main h3 {
margin-bottom: 0.75em
}
body#blog_post_show .post-tags .label-default {
background-color: #e9ecec;
color: #6D8283;
font-size: 16px;
margin-right: 10px;
padding: .4em 1em .5em;
}
body#blog_post_show .post-tags .label-default i {
color: #95A6A7;
}
body#blog_post_show #post-add-comment {
margin: 2em 0
}
body#blog_post_show #post-add-comment p {
margin-bottom: 0
}
body#blog_post_show #post-add-comment p a.btn {
margin-right: 0.5em
}
body#blog_post_show .post-comment {
margin-bottom: 2em
}
body#blog_post_show .post-comment h4 {
font-size: 13px;
line-height: 1.42857;
margin-top: 0
}
body#blog_post_show .post-comment h4 strong {
display: block
}
/* Page: 'Comment form error'
------------------------------------------------------------------------- */
body#comment_form_error h1.text-danger {
margin-bottom: 1em
}
@media (min-width: 768px) and (max-width: 1200px) {
.container {
width: 98%;
}
}
/* Page: 'Blog search'
------------------------------------------------------------------------- */
body#blog_search #main h1,
body#blog_search #main p {
margin-bottom: 0.5em
}
body#blog_search article.post:first-child {
margin-top: 2em;
}
body#blog_search article.post {
margin-bottom: 2em;
}
body#blog_search .post-metadata {
font-size: 16px;
margin-bottom: 8px;
}

194
app/assets/scss/bootstrap-tagsinput.scss vendored Normal file
View File

@ -0,0 +1,194 @@
/* ------------------------------------------------------------------------------
*
* # Twiter Typeahead
*
* Styles for tagsinput.js - input suggestion engine
*
* ---------------------------------------------------------------------------- */
.twitter-typeahead {
width: 100%;
}
.typeahead,
.tt-query,
.tt-hint {
outline: 0;
}
.tt-hint {
color: #999;
}
.tt-menu{
width: 100%;
margin-top: 1px;
min-width: 180px;
padding: 7px 0;
background-color: #fff;
border: 1px solid rgba(0,0,0,0.15);
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
-webkit-box-shadow: 0 6px 12px rgba(0,0,0,0.175);
box-shadow: 0 6px 12px rgba(0,0,0,0.175);
-webkit-background-clip: padding-box;
background-clip: padding-box;
}
.typeahead-scrollable .tt-menu{
max-height: 250px;
}
.typeahead-rtl .tt-menu{
text-align: right;
}
.tt-suggestion {
padding: 8px 15px;
cursor: pointer;
}
.tt-suggestion.tt-cursor {
background-color: #f5f5f5;
}
.tt-suggestion p {
margin: 0;
}
.tt-suggestion.tt-selectable:before {
content: '\f02b';
font-family: 'Font Awesome 5 Free';
font-weight: 900;
display: inline-block;
font-size: 15px;
margin-right: 0.5em;
color: inherit;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.tt-dataset-group .tt-suggestion {
padding-left: 24px;
padding-right: 24px;
}
.tt-heading {
font-size: 11px;
line-height: 1.82;
padding: 8px 15px;
text-transform: uppercase;
display: block;
font-weight: 700;
margin-top: 2px;
margin-bottom: 2px;
}
.tt-suggestion:hover,
.tt-suggestion:focus {
color: #ffffff;
text-decoration: none;
outline: 0;
background-color: #18bc9c;
}
/* ------------------------------------------------------------------------------
*
* # Bootstrap tags input
*
* Styles for tagsinput.js - tags input for Bootstrap
*
* ---------------------------------------------------------------------------- */
.bootstrap-tagsinput {
display: table-cell;
vertical-align: middle;
width: 100%;
height: 45px;
padding: 0;
font-size: 15px;
line-height: 1.42857143;
color: #2c3e50;
background-color: #ffffff;
background-image: none;
border: 2px solid #dce4ec;
border-radius: 4px;
border-bottom-right-radius: 0;
border-top-right-radius: 0;
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
}
.has-error .bootstrap-tagsinput {
border-color: #e74c3c !important;
}
.bootstrap-tagsinput.focus {
border-color: #2c3e50;
outline: 0;
box-shadow: none;
}
.bootstrap-tagsinput input {
border: 0;
outline: 0;
background-color: transparent;
padding: 5px 11px;
margin-top: 2px;
margin-left: 2px;
width: auto !important;
min-width: 100px;
font-size: 15px;
line-height: 1.6666667;
-webkit-box-shadow: none;
box-shadow: none;
}
.bootstrap-tagsinput input:focus {
border: none;
box-shadow: none;
}
.bootstrap-tagsinput .twitter-typeahead {
width: auto;
}
.bootstrap-tagsinput .tt-menu {
margin-top: 5px;
min-width: 200px;
}
.bootstrap-tagsinput .tag {
margin: 1px 0 0 3px;
border: 0;
border-radius: .25em;
padding: 5px 11px;
padding-right: 30px;
float: left;
font-size: 15px;
line-height: 1.6666667;
font-weight: 400;
text-transform: none;
position: relative;
background-color: #18bc9c;
color: #fff;
}
.has-error .bootstrap-tagsinput .tag {
background-color: #e74c3c !important;
}
.bootstrap-tagsinput .tag [data-role="remove"] {
cursor: pointer;
color: inherit;
position: absolute;
top: 50%;
right: 11px;
line-height: 1;
margin-top: -5.5px;
opacity: 0.7;
filter: alpha(opacity=70);
}
.bootstrap-tagsinput .tag [data-role="remove"]:hover {
opacity: 1;
filter: alpha(opacity=100);
}
.bootstrap-tagsinput .tag:before {
content: '\f02b';
font-family: 'Font Awesome 5 Free';
font-weight: 900;
display: inline-block;
font-size: 15px;
margin-right: 0.5em;
color: #fff;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.bootstrap-tagsinput .tag [data-role="remove"]:after {
content: '\f00d';
font-family: 'Font Awesome 5 Free';
font-weight: 900;
display: block;
font-size: 13px;
color: #fff;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

43
app/bin/console Executable file
View File

@ -0,0 +1,43 @@
#!/usr/bin/env php
<?php
use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Dotenv\Dotenv;
use Symfony\Component\ErrorHandler\Debug;
if (!in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
echo 'Warning: The console should be invoked via the CLI version of PHP, not the '.PHP_SAPI.' SAPI'.PHP_EOL;
}
set_time_limit(0);
require dirname(__DIR__).'/vendor/autoload.php';
if (!class_exists(Application::class) || !class_exists(Dotenv::class)) {
throw new LogicException('You need to add "symfony/framework-bundle" and "symfony/dotenv" as Composer dependencies.');
}
$input = new ArgvInput();
if (null !== $env = $input->getParameterOption(['--env', '-e'], null, true)) {
putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env);
}
if ($input->hasParameterOption('--no-debug', true)) {
putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0');
}
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
if ($_SERVER['APP_DEBUG']) {
umask(0000);
if (class_exists(Debug::class)) {
Debug::enable();
}
}
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$application = new Application($kernel);
$application->run($input);

13
app/bin/phpunit Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env php
<?php
if (!file_exists(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
exit(1);
}
if (false === getenv('SYMFONY_PHPUNIT_DIR')) {
putenv('SYMFONY_PHPUNIT_DIR='.__DIR__.'/.phpunit');
}
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';

92
app/composer.json Normal file
View File

@ -0,0 +1,92 @@
{
"license": "proprietary",
"type": "project",
"minimum-stability": "stable",
"prefer-stable": true,
"replace": {
"symfony/polyfill-php70": "*",
"symfony/polyfill-php72": "*"
},
"require": {
"php": "^7.2.9",
"ext-pdo_sqlite": "*",
"composer/package-versions-deprecated": "^1.8",
"doctrine/doctrine-bundle": "^1.12|^2.0",
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.5.11",
"erusev/parsedown": "^1.6",
"sensio/framework-extra-bundle": "^5.6",
"symfony/apache-pack": "^1.0",
"symfony/asset": "^5.2",
"symfony/console": "^5.2",
"symfony/dotenv": "^5.2",
"symfony/expression-language": "^5.2",
"symfony/flex": "^1.1",
"symfony/form": "^5.2",
"symfony/framework-bundle": "^5.2",
"symfony/intl": "^5.2",
"symfony/mailer": "^5.2",
"symfony/monolog-bundle": "^3.1",
"symfony/polyfill-intl-messageformatter": "^1.12",
"symfony/security-bundle": "^5.2",
"symfony/string": "^5.2",
"symfony/translation": "^5.2",
"symfony/twig-pack": "^1.0",
"symfony/validator": "^5.2",
"symfony/webpack-encore-bundle": "^1.4",
"symfony/yaml": "^5.2",
"tgalopin/html-sanitizer-bundle": "^1.2",
"twig/intl-extra": "^3.0",
"twig/markdown-extra": "^3.0"
},
"require-dev": {
"dama/doctrine-test-bundle": "^6.2",
"doctrine/doctrine-fixtures-bundle": "^3.0",
"symfony/browser-kit": "^5.2",
"symfony/css-selector": "^5.2",
"symfony/debug-bundle": "^5.2",
"symfony/maker-bundle": "^1.11",
"symfony/phpunit-bridge": "^5.2",
"symfony/stopwatch": "^5.2",
"symfony/web-profiler-bundle": "^5.2"
},
"config": {
"platform": {
"php": "7.2.9"
},
"preferred-install": {
"*": "dist"
},
"sort-packages": true
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install --symlink --relative %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": true
}
}
}

8273
app/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

19
app/config/bundles.php Normal file
View File

@ -0,0 +1,19 @@
<?php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
HtmlSanitizer\Bundle\HtmlSanitizerBundle::class => ['all' => true],
];

View File

@ -0,0 +1,3 @@
framework:
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'

View File

@ -0,0 +1,19 @@
framework:
cache:
# Unique name of your app: used to compute stable namespaces for cache keys.
#prefix_seed: your_vendor_name/app_name
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null

View File

@ -0,0 +1,4 @@
debug:
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
# See the "server:dump" command to start a new server.
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"

View File

@ -0,0 +1,4 @@
framework:
mailer:
# this disables delivery of messages entirely
dsn: 'null://null'

View File

@ -0,0 +1,19 @@
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event"]
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:
# type: firephp
# level: info
#chromephp:
# type: chromephp
# level: info
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]

View File

@ -0,0 +1,6 @@
web_profiler:
toolbar: true
intercept_redirects: false
framework:
profiler: { only_exceptions: false }

View File

@ -0,0 +1,18 @@
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
#server_version: '5.7'
orm:
auto_generate_proxy_classes: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
mappings:
App:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App

View File

@ -0,0 +1,5 @@
doctrine_migrations:
migrations_paths:
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations'

View File

@ -0,0 +1,27 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html
framework:
secret: '%env(APP_SECRET)%'
csrf_protection: true
http_method_override: true
# Enables session support. Note that the session will ONLY be started if you read or write from it.
# Remove or comment this section to explicitly disable session support.
session:
handler_id: null
cookie_secure: auto
cookie_samesite: lax
# When using the HTTP Cache, ESI allows to render page fragments separately
# and with different cache configurations for each fragment
# https://symfony.com/doc/current/http_cache/esi.html
esi: true
fragments: true
php_errors:
log: true
# The 'ide' option turns all of the file paths in an exception page
# into clickable links that open the given file using your favorite IDE.
# When 'ide' is set to null the file is opened in your web browser.
# See https://symfony.com/doc/current/reference/configuration/framework.html#ide
ide: null

View File

@ -0,0 +1,17 @@
html_sanitizer:
default_sanitizer: 'default'
sanitizers:
default:
# Read https://github.com/tgalopin/html-sanitizer/blob/master/docs/1-getting-started.md#extensions
# to learn more about which extensions you would like to enable.
extensions:
- 'basic'
- 'list'
- 'table'
- 'image'
- 'code'
# - 'iframe'
# - 'extra'
# Read https://github.com/tgalopin/html-sanitizer/blob/master/docs/3-configuration-reference.md
# to discover all the available options for each extension.

View File

@ -0,0 +1,3 @@
framework:
mailer:
dsn: '%env(MAILER_DSN)%'

View File

@ -0,0 +1,8 @@
# As of Symfony 5.1, deprecations are logged in the dedicated "deprecation" channel when it exists
#monolog:
# channels: [deprecation]
# handlers:
# deprecation:
# type: stream
# channels: [deprecation]
# path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log"

View File

@ -0,0 +1,20 @@
doctrine:
orm:
auto_generate_proxy_classes: false
metadata_cache_driver:
type: pool
pool: doctrine.system_cache_pool
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

View File

@ -0,0 +1,16 @@
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine"]

View File

@ -0,0 +1,3 @@
framework:
router:
strict_requirements: null

View File

@ -0,0 +1,4 @@
#webpack_encore:
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
# Available in version 1.2
#cache: true

View File

@ -0,0 +1,7 @@
framework:
router:
utf8: true
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
#default_uri: http://localhost

View File

@ -0,0 +1,63 @@
security:
encoders:
# Our user class and the algorithm we'll use to encode passwords
# 'auto' means to let Symfony choose the best possible password hasher (Argon2 or Bcrypt)
# https://symfony.com/doc/current/security.html#c-encoding-passwords
App\Entity\User: 'auto'
providers:
# https://symfony.com/doc/current/security/user_provider.html
# In this example, users are stored via Doctrine in the database
# To see the users at src/App/DataFixtures/ORM/LoadFixtures.php
# To load users from somewhere else: https://symfony.com/doc/current/security/user_provider.html#creating-a-custom-user-provider
database_users:
entity: { class: App\Entity\User, property: username }
# https://symfony.com/doc/current/security.html#a-authentication-firewalls
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
# this firewall applies to all URLs
pattern: ^/
# but the firewall does not require login on every page
# denying access is done in access_control or in your controllers
anonymous: true
lazy: true
# The user provider to use.
provider: database_users
# This allows the user to login by submitting a username and password
# Reference: https://symfony.com/doc/current/security/form_login_setup.html
form_login:
# The route name that the login form submits to
check_path: security_login
# The name of the route where the login form lives
# When the user tries to access a protected page, they are redirected here
login_path: security_login
# Secure the login form against CSRF
# Reference: https://symfony.com/doc/current/security/csrf.html#csrf-protection-in-login-forms
csrf_token_generator: security.csrf.token_manager
# The page users are redirect to when there is no previous page stored in the
# session (for example when the users access directly to the login page).
default_target_path: blog_index
logout:
# The route name the user can go to in order to logout
path: security_logout
# The name of the route to redirect to after logging out
target: homepage
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# this is a catch-all for the admin area
# additional security lives in the controllers
- { path: '^/(%app_locales%)/admin', roles: ROLE_ADMIN }
role_hierarchy:
ROLE_ADMIN: ROLE_USER

View File

@ -0,0 +1,3 @@
sensio_framework_extra:
router:
annotations: false

View File

@ -0,0 +1,4 @@
dama_doctrine_test:
enable_static_connection: true
enable_static_meta_data_cache: true
enable_static_query_cache: true

View File

@ -0,0 +1,4 @@
framework:
test: true
session:
storage_id: session.storage.mock_file

View File

@ -0,0 +1,4 @@
framework:
mailer:
# this disables delivery of messages entirely
dsn: 'null://null'

View File

@ -0,0 +1,12 @@
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
channels: ["!event"]
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug

View File

@ -0,0 +1,6 @@
# this configuration simplifies testing URLs protected by the security mechanism
# See https://symfony.com/doc/current/testing/http_authentication.html
security:
firewalls:
main:
http_basic: ~

View File

@ -0,0 +1,2 @@
twig:
strict_variables: true

View File

@ -0,0 +1,3 @@
framework:
validation:
not_compromised_password: false

View File

@ -0,0 +1,6 @@
web_profiler:
toolbar: false
intercept_redirects: false
framework:
profiler: { collect: false }

View File

@ -0,0 +1,2 @@
#webpack_encore:
# strict_mode: false

View File

@ -0,0 +1,6 @@
framework:
default_locale: '%locale%'
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- '%locale%'

View File

@ -0,0 +1,5 @@
twig:
default_path: '%kernel.project_dir%/templates'
form_themes:
- 'form/layout.html.twig'
- 'form/fields.html.twig'

View File

@ -0,0 +1,9 @@
framework:
validation:
enable_annotations: true
email_validation_mode: html5
# Enables validator auto-mapping support.
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
auto_mapping:
App\Entity\: []

View File

@ -0,0 +1,25 @@
webpack_encore:
# The path where Encore is building the assets - i.e. Encore.setOutputPath()
output_path: '%kernel.project_dir%/public/build'
# If multiple builds are defined (as shown below), you can disable the default build:
# output_path: false
# if using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
# crossorigin: 'anonymous'
# preload all rendered script and link tags automatically via the http2 Link header
# preload: true
# Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
# strict_mode: false
# if you have multiple builds:
# builds:
# pass "frontend" as the 3rg arg to the Twig functions
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
# frontend: '%kernel.project_dir%/public/frontend/build'
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
# Put in config/packages/prod/webpack_encore.yaml
# cache: true

5
app/config/preload.php Normal file
View File

@ -0,0 +1,5 @@
<?php
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
}

12
app/config/routes.yaml Normal file
View File

@ -0,0 +1,12 @@
# These lines define a route using YAML configuration. The controller used by
# the route (FrameworkBundle:Template:template) is a convenient shortcut when
# the template can be rendered without executing any logic in your own controller.
# See https://symfony.com/doc/current/templates.html#rendering-a-template-directly-from-a-route
homepage:
path: /{_locale}
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController::templateAction
requirements:
_locale: '%app_locales%'
defaults:
template: default/homepage.html.twig
_locale: '%locale%'

View File

@ -0,0 +1,8 @@
controllers:
resource: '../../src/Controller/'
type: annotation
prefix: /{_locale}
requirements:
_locale: '%app_locales%'
defaults:
_locale: '%locale%'

View File

@ -0,0 +1,3 @@
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
prefix: /_error

View File

@ -0,0 +1,7 @@
web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
prefix: /_profiler

41
app/config/services.yaml Normal file
View File

@ -0,0 +1,41 @@
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
locale: 'en'
# This parameter defines the codes of the locales (languages) enabled in the application
app_locales: ar|en|fr|de|es|cs|nl|ru|uk|ro|pt_BR|pl|it|ja|id|ca|sl|hr|zh_CN|bg|tr|lt
app.notifications.email_sender: anonymous@example.com
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
bind: # defines the scalar arguments once and apply them to any service defined/created in this file
string $locales: '%app_locales%'
string $defaultLocale: '%locale%'
string $emailSender: '%app.notifications.email_sender%'
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
- '../src/Tests/'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller/'
tags: ['controller.service_arguments']
# when the service definition only contains arguments, you can omit the
# 'arguments' key and define the arguments just below the service class
App\EventSubscriber\CommentNotificationSubscriber:
$sender: '%app.notifications.email_sender%'

0
app/migrations/.gitignore vendored Normal file
View File

27
app/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"devDependencies": {
"@fortawesome/fontawesome-free": "^5.8.1",
"@symfony/webpack-encore": "^0.31.0",
"bloodhound-js": "^1.2.3",
"bootstrap-sass": "^3.3.7",
"bootstrap-tagsinput": "^0.7.1",
"bootswatch": "^3.3.7",
"core-js": "^3.0.0",
"eonasdan-bootstrap-datetimepicker": "^4.17.47",
"highlight.js": "^10.4.1",
"imports-loader": "^0.8.0",
"jquery": "^3.5.1",
"lato-font": "^3.0.0",
"node-sass": "^4.9.3",
"sass-loader": "^9.0.1",
"typeahead.js": "^0.11.1"
},
"license": "MIT",
"private": true,
"scripts": {
"dev-server": "encore dev-server",
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress"
}
}

39
app/phpunit.xml.dist Normal file
View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="bin/.phpunit/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/bootstrap.php"
>
<php>
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
<server name="SYMFONY_PHPUNIT_REMOVE" value="" />
<server name="SYMFONY_PHPUNIT_VERSION" value="8" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>src</directory>
</whitelist>
</filter>
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
</listeners>
<extensions>
<!-- it begins a database transaction before every testcase and rolls it back after
the test finished, so tests can manipulate the database without affecting other tests -->
<extension class="\DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" />
</extensions>
</phpunit>

66
app/public/.htaccess Normal file
View File

@ -0,0 +1,66 @@
# Use the front controller as index file. It serves as a fallback solution when
# every other rewrite/redirect fails (e.g. in an aliased environment without
# mod_rewrite). Additionally, this reduces the matching process for the
# start page (path "/") because otherwise Apache will apply the rewriting rules
# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
DirectoryIndex index.php
# By default, Apache does not evaluate symbolic links if you did not enable this
# feature in your server configuration. Uncomment the following line if you
# install assets as symlinks or if you experience problems related to symlinks
# when compiling LESS/Sass/CoffeScript assets.
# Options +FollowSymlinks
# Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve
# to the front controller "/index.php" but be rewritten to "/index.php/index".
<IfModule mod_negotiation.c>
Options -MultiViews
</IfModule>
<IfModule mod_rewrite.c>
RewriteEngine On
# Determine the RewriteBase automatically and set it as environment variable.
# If you are using Apache aliases to do mass virtual hosting or installed the
# project in a subdirectory, the base path will be prepended to allow proper
# resolution of the index.php file and to redirect to the correct URI. It will
# work in environments without path prefix as well, providing a safe, one-size
# fits all solution. But as you do not need it in this case, you can comment
# the following 2 lines to eliminate the overhead.
RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
RewriteRule .* - [E=BASE:%1]
# Sets the HTTP_AUTHORIZATION header removed by Apache
RewriteCond %{HTTP:Authorization} .+
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
# Redirect to URI without front controller to prevent duplicate content
# (with and without `/index.php`). Only do this redirect on the initial
# rewrite by Apache and not on subsequent cycles. Otherwise we would get an
# endless redirect loop (request -> rewrite to front controller ->
# redirect -> request -> ...).
# So in case you get a "too many redirects" error or you always get redirected
# to the start page because your Apache does not expose the REDIRECT_STATUS
# environment variable, you have 2 choices:
# - disable this feature by commenting the following 2 lines or
# - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
# following RewriteCond (best solution)
RewriteCond %{ENV:REDIRECT_STATUS} =""
RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
# If the requested filename exists, simply serve it.
# We only want to let Apache serve files and not directories.
# Rewrite all other queries to the front controller.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ %{ENV:BASE}/index.php [L]
</IfModule>
<IfModule !mod_rewrite.c>
<IfModule mod_alias.c>
# When mod_rewrite is not available, we instruct a temporary redirect of
# the start page to the front controller explicitly so that the website
# and the generated links can still be used.
RedirectMatch 307 ^/$ /index.php/
# RedirectTemp cannot be used instead
</IfModule>
</IfModule>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
app/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

30
app/public/index.php Normal file
View File

@ -0,0 +1,30 @@
<?php
use App\Kernel;
use Symfony\Component\Dotenv\Dotenv;
use Symfony\Component\ErrorHandler\Debug;
use Symfony\Component\HttpFoundation\Request;
require dirname(__DIR__).'/vendor/autoload.php';
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
if ($_SERVER['APP_DEBUG']) {
umask(0000);
Debug::enable();
}
if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) {
Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST);
}
if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) {
Request::setTrustedHosts([$trustedHosts]);
}
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

4
app/public/robots.txt Normal file
View File

@ -0,0 +1,4 @@
# www.robotstxt.org/
# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449
User-agent: *

View File

@ -0,0 +1,263 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Command;
use App\Entity\User;
use App\Repository\UserRepository;
use App\Utils\Validator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Stopwatch\Stopwatch;
use function Symfony\Component\String\u;
/**
* A console command that creates users and stores them in the database.
*
* To use this command, open a terminal window, enter into your project
* directory and execute the following:
*
* $ php bin/console app:add-user
*
* To output detailed information, increase the command verbosity:
*
* $ php bin/console app:add-user -vv
*
* See https://symfony.com/doc/current/console.html
*
* We use the default services.yaml configuration, so command classes are registered as services.
* See https://symfony.com/doc/current/console/commands_as_services.html
*
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class AddUserCommand extends Command
{
// to make your command lazily loaded, configure the $defaultName static property,
// so it will be instantiated only when the command is actually called.
protected static $defaultName = 'app:add-user';
/**
* @var SymfonyStyle
*/
private $io;
private $entityManager;
private $passwordEncoder;
private $validator;
private $users;
public function __construct(EntityManagerInterface $em, UserPasswordEncoderInterface $encoder, Validator $validator, UserRepository $users)
{
parent::__construct();
$this->entityManager = $em;
$this->passwordEncoder = $encoder;
$this->validator = $validator;
$this->users = $users;
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDescription('Creates users and stores them in the database')
->setHelp($this->getCommandHelp())
// commands can optionally define arguments and/or options (mandatory and optional)
// see https://symfony.com/doc/current/components/console/console_arguments.html
->addArgument('username', InputArgument::OPTIONAL, 'The username of the new user')
->addArgument('password', InputArgument::OPTIONAL, 'The plain password of the new user')
->addArgument('email', InputArgument::OPTIONAL, 'The email of the new user')
->addArgument('full-name', InputArgument::OPTIONAL, 'The full name of the new user')
->addOption('admin', null, InputOption::VALUE_NONE, 'If set, the user is created as an administrator')
;
}
/**
* This optional method is the first one executed for a command after configure()
* and is useful to initialize properties based on the input arguments and options.
*/
protected function initialize(InputInterface $input, OutputInterface $output): void
{
// SymfonyStyle is an optional feature that Symfony provides so you can
// apply a consistent look to the commands of your application.
// See https://symfony.com/doc/current/console/style.html
$this->io = new SymfonyStyle($input, $output);
}
/**
* This method is executed after initialize() and before execute(). Its purpose
* is to check if some of the options/arguments are missing and interactively
* ask the user for those values.
*
* This method is completely optional. If you are developing an internal console
* command, you probably should not implement this method because it requires
* quite a lot of work. However, if the command is meant to be used by external
* users, this method is a nice way to fall back and prevent errors.
*/
protected function interact(InputInterface $input, OutputInterface $output)
{
if (null !== $input->getArgument('username') && null !== $input->getArgument('password') && null !== $input->getArgument('email') && null !== $input->getArgument('full-name')) {
return;
}
$this->io->title('Add User Command Interactive Wizard');
$this->io->text([
'If you prefer to not use this interactive wizard, provide the',
'arguments required by this command as follows:',
'',
' $ php bin/console app:add-user username password email@example.com',
'',
'Now we\'ll ask you for the value of all the missing command arguments.',
]);
// Ask for the username if it's not defined
$username = $input->getArgument('username');
if (null !== $username) {
$this->io->text(' > <info>Username</info>: '.$username);
} else {
$username = $this->io->ask('Username', null, [$this->validator, 'validateUsername']);
$input->setArgument('username', $username);
}
// Ask for the password if it's not defined
$password = $input->getArgument('password');
if (null !== $password) {
$this->io->text(' > <info>Password</info>: '.u('*')->repeat(u($password)->length()));
} else {
$password = $this->io->askHidden('Password (your type will be hidden)', [$this->validator, 'validatePassword']);
$input->setArgument('password', $password);
}
// Ask for the email if it's not defined
$email = $input->getArgument('email');
if (null !== $email) {
$this->io->text(' > <info>Email</info>: '.$email);
} else {
$email = $this->io->ask('Email', null, [$this->validator, 'validateEmail']);
$input->setArgument('email', $email);
}
// Ask for the full name if it's not defined
$fullName = $input->getArgument('full-name');
if (null !== $fullName) {
$this->io->text(' > <info>Full Name</info>: '.$fullName);
} else {
$fullName = $this->io->ask('Full Name', null, [$this->validator, 'validateFullName']);
$input->setArgument('full-name', $fullName);
}
}
/**
* This method is executed after interact() and initialize(). It usually
* contains the logic to execute to complete this command task.
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$stopwatch = new Stopwatch();
$stopwatch->start('add-user-command');
$username = $input->getArgument('username');
$plainPassword = $input->getArgument('password');
$email = $input->getArgument('email');
$fullName = $input->getArgument('full-name');
$isAdmin = $input->getOption('admin');
// make sure to validate the user data is correct
$this->validateUserData($username, $plainPassword, $email, $fullName);
// create the user and encode its password
$user = new User();
$user->setFullName($fullName);
$user->setUsername($username);
$user->setEmail($email);
$user->setRoles([$isAdmin ? 'ROLE_ADMIN' : 'ROLE_USER']);
// See https://symfony.com/doc/current/security.html#c-encoding-passwords
$encodedPassword = $this->passwordEncoder->encodePassword($user, $plainPassword);
$user->setPassword($encodedPassword);
$this->entityManager->persist($user);
$this->entityManager->flush();
$this->io->success(sprintf('%s was successfully created: %s (%s)', $isAdmin ? 'Administrator user' : 'User', $user->getUsername(), $user->getEmail()));
$event = $stopwatch->stop('add-user-command');
if ($output->isVerbose()) {
$this->io->comment(sprintf('New user database id: %d / Elapsed time: %.2f ms / Consumed memory: %.2f MB', $user->getId(), $event->getDuration(), $event->getMemory() / (1024 ** 2)));
}
return Command::SUCCESS;
}
private function validateUserData($username, $plainPassword, $email, $fullName): void
{
// first check if a user with the same username already exists.
$existingUser = $this->users->findOneBy(['username' => $username]);
if (null !== $existingUser) {
throw new RuntimeException(sprintf('There is already a user registered with the "%s" username.', $username));
}
// validate password and email if is not this input means interactive.
$this->validator->validatePassword($plainPassword);
$this->validator->validateEmail($email);
$this->validator->validateFullName($fullName);
// check if a user with the same email already exists.
$existingEmail = $this->users->findOneBy(['email' => $email]);
if (null !== $existingEmail) {
throw new RuntimeException(sprintf('There is already a user registered with the "%s" email.', $email));
}
}
/**
* The command help is usually included in the configure() method, but when
* it's too long, it's better to define a separate method to maintain the
* code readability.
*/
private function getCommandHelp(): string
{
return <<<'HELP'
The <info>%command.name%</info> command creates new users and saves them in the database:
<info>php %command.full_name%</info> <comment>username password email</comment>
By default the command creates regular users. To create administrator users,
add the <comment>--admin</comment> option:
<info>php %command.full_name%</info> username password email <comment>--admin</comment>
If you omit any of the three required arguments, the command will ask you to
provide the missing values:
# command will ask you for the email
<info>php %command.full_name%</info> <comment>username password</comment>
# command will ask you for the email and password
<info>php %command.full_name%</info> <comment>username</comment>
# command will ask you for all arguments
<info>php %command.full_name%</info>
HELP;
}
}

View File

@ -0,0 +1,132 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Command;
use App\Entity\User;
use App\Repository\UserRepository;
use App\Utils\Validator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* A console command that deletes users from the database.
*
* To use this command, open a terminal window, enter into your project
* directory and execute the following:
*
* $ php bin/console app:delete-user
*
* Check out the code of the src/Command/AddUserCommand.php file for
* the full explanation about Symfony commands.
*
* See https://symfony.com/doc/current/console.html
*
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
*/
class DeleteUserCommand extends Command
{
protected static $defaultName = 'app:delete-user';
/** @var SymfonyStyle */
private $io;
private $entityManager;
private $validator;
private $users;
public function __construct(EntityManagerInterface $em, Validator $validator, UserRepository $users)
{
parent::__construct();
$this->entityManager = $em;
$this->validator = $validator;
$this->users = $users;
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDescription('Deletes users from the database')
->addArgument('username', InputArgument::REQUIRED, 'The username of an existing user')
->setHelp(<<<'HELP'
The <info>%command.name%</info> command deletes users from the database:
<info>php %command.full_name%</info> <comment>username</comment>
If you omit the argument, the command will ask you to
provide the missing value:
<info>php %command.full_name%</info>
HELP
);
}
protected function initialize(InputInterface $input, OutputInterface $output): void
{
// SymfonyStyle is an optional feature that Symfony provides so you can
// apply a consistent look to the commands of your application.
// See https://symfony.com/doc/current/console/style.html
$this->io = new SymfonyStyle($input, $output);
}
protected function interact(InputInterface $input, OutputInterface $output)
{
if (null !== $input->getArgument('username')) {
return;
}
$this->io->title('Delete User Command Interactive Wizard');
$this->io->text([
'If you prefer to not use this interactive wizard, provide the',
'arguments required by this command as follows:',
'',
' $ php bin/console app:delete-user username',
'',
'Now we\'ll ask you for the value of all the missing command arguments.',
'',
]);
$username = $this->io->ask('Username', null, [$this->validator, 'validateUsername']);
$input->setArgument('username', $username);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$username = $this->validator->validateUsername($input->getArgument('username'));
/** @var User $user */
$user = $this->users->findOneByUsername($username);
if (null === $user) {
throw new RuntimeException(sprintf('User with username "%s" not found.', $username));
}
// After an entity has been removed its in-memory state is the same
// as before the removal, except for generated identifiers.
// See https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-objects.html#removing-entities
$userId = $user->getId();
$this->entityManager->remove($user);
$this->entityManager->flush();
$this->io->success(sprintf('User "%s" (ID: %d, email: %s) was successfully deleted.', $user->getUsername(), $userId, $user->getEmail()));
return Command::SUCCESS;
}
}

View File

@ -0,0 +1,146 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Command;
use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
/**
* A console command that lists all the existing users.
*
* To use this command, open a terminal window, enter into your project directory
* and execute the following:
*
* $ php bin/console app:list-users
*
* Check out the code of the src/Command/AddUserCommand.php file for
* the full explanation about Symfony commands.
*
* See https://symfony.com/doc/current/console.html
*
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
class ListUsersCommand extends Command
{
// a good practice is to use the 'app:' prefix to group all your custom application commands
protected static $defaultName = 'app:list-users';
private $mailer;
private $emailSender;
private $users;
public function __construct(MailerInterface $mailer, string $emailSender, UserRepository $users)
{
parent::__construct();
$this->mailer = $mailer;
$this->emailSender = $emailSender;
$this->users = $users;
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDescription('Lists all the existing users')
->setHelp(<<<'HELP'
The <info>%command.name%</info> command lists all the users registered in the application:
<info>php %command.full_name%</info>
By default the command only displays the 50 most recent users. Set the number of
results to display with the <comment>--max-results</comment> option:
<info>php %command.full_name%</info> <comment>--max-results=2000</comment>
In addition to displaying the user list, you can also send this information to
the email address specified in the <comment>--send-to</comment> option:
<info>php %command.full_name%</info> <comment>--send-to=fabien@symfony.com</comment>
HELP
)
// commands can optionally define arguments and/or options (mandatory and optional)
// see https://symfony.com/doc/current/components/console/console_arguments.html
->addOption('max-results', null, InputOption::VALUE_OPTIONAL, 'Limits the number of users listed', 50)
->addOption('send-to', null, InputOption::VALUE_OPTIONAL, 'If set, the result is sent to the given email address')
;
}
/**
* This method is executed after initialize(). It usually contains the logic
* to execute to complete this command task.
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$maxResults = $input->getOption('max-results');
// Use ->findBy() instead of ->findAll() to allow result sorting and limiting
$allUsers = $this->users->findBy([], ['id' => 'DESC'], $maxResults);
// Doctrine query returns an array of objects and we need an array of plain arrays
$usersAsPlainArrays = array_map(function (User $user) {
return [
$user->getId(),
$user->getFullName(),
$user->getUsername(),
$user->getEmail(),
implode(', ', $user->getRoles()),
];
}, $allUsers);
// In your console commands you should always use the regular output type,
// which outputs contents directly in the console window. However, this
// command uses the BufferedOutput type instead, to be able to get the output
// contents before displaying them. This is needed because the command allows
// to send the list of users via email with the '--send-to' option
$bufferedOutput = new BufferedOutput();
$io = new SymfonyStyle($input, $bufferedOutput);
$io->table(
['ID', 'Full Name', 'Username', 'Email', 'Roles'],
$usersAsPlainArrays
);
// instead of just displaying the table of users, store its contents in a variable
$usersAsATable = $bufferedOutput->fetch();
$output->write($usersAsATable);
if (null !== $email = $input->getOption('send-to')) {
$this->sendReport($usersAsATable, $email);
}
return Command::SUCCESS;
}
/**
* Sends the given $contents to the $recipient email address.
*/
private function sendReport(string $contents, string $recipient): void
{
$email = (new Email())
->from($this->emailSender)
->to($recipient)
->subject(sprintf('app:list-users report (%s)', date('Y-m-d H:i:s')))
->text($contents);
$this->mailer->send($email);
}
}

View File

@ -0,0 +1,177 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Controller\Admin;
use App\Entity\Post;
use App\Form\PostType;
use App\Repository\PostRepository;
use App\Security\PostVoter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* Controller used to manage blog contents in the backend.
*
* Please note that the application backend is developed manually for learning
* purposes. However, in your real Symfony application you should use any of the
* existing bundles that let you generate ready-to-use backends without effort.
*
* See http://knpbundles.com/keyword/admin
*
* @Route("/admin/post")
* @IsGranted("ROLE_ADMIN")
*
* @author Ryan Weaver <weaverryan@gmail.com>
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
class BlogController extends AbstractController
{
/**
* Lists all Post entities.
*
* This controller responds to two different routes with the same URL:
* * 'admin_post_index' is the route with a name that follows the same
* structure as the rest of the controllers of this class.
* * 'admin_index' is a nice shortcut to the backend homepage. This allows
* to create simpler links in the templates. Moreover, in the future we
* could move this annotation to any other controller while maintaining
* the route name and therefore, without breaking any existing link.
*
* @Route("/", methods="GET", name="admin_index")
* @Route("/", methods="GET", name="admin_post_index")
*/
public function index(PostRepository $posts): Response
{
$authorPosts = $posts->findBy(['author' => $this->getUser()], ['publishedAt' => 'DESC']);
return $this->render('admin/blog/index.html.twig', ['posts' => $authorPosts]);
}
/**
* Creates a new Post entity.
*
* @Route("/new", methods="GET|POST", name="admin_post_new")
*
* NOTE: the Method annotation is optional, but it's a recommended practice
* to constraint the HTTP methods each controller responds to (by default
* it responds to all methods).
*/
public function new(Request $request): Response
{
$post = new Post();
$post->setAuthor($this->getUser());
// See https://symfony.com/doc/current/form/multiple_buttons.html
$form = $this->createForm(PostType::class, $post)
->add('saveAndCreateNew', SubmitType::class);
$form->handleRequest($request);
// the isSubmitted() method is completely optional because the other
// isValid() method already checks whether the form is submitted.
// However, we explicitly add it to improve code readability.
// See https://symfony.com/doc/current/forms.html#processing-forms
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($post);
$em->flush();
// Flash messages are used to notify the user about the result of the
// actions. They are deleted automatically from the session as soon
// as they are accessed.
// See https://symfony.com/doc/current/controller.html#flash-messages
$this->addFlash('success', 'post.created_successfully');
if ($form->get('saveAndCreateNew')->isClicked()) {
return $this->redirectToRoute('admin_post_new');
}
return $this->redirectToRoute('admin_post_index');
}
return $this->render('admin/blog/new.html.twig', [
'post' => $post,
'form' => $form->createView(),
]);
}
/**
* Finds and displays a Post entity.
*
* @Route("/{id<\d+>}", methods="GET", name="admin_post_show")
*/
public function show(Post $post): Response
{
// This security check can also be performed
// using an annotation: @IsGranted("show", subject="post", message="Posts can only be shown to their authors.")
$this->denyAccessUnlessGranted(PostVoter::SHOW, $post, 'Posts can only be shown to their authors.');
return $this->render('admin/blog/show.html.twig', [
'post' => $post,
]);
}
/**
* Displays a form to edit an existing Post entity.
*
* @Route("/{id<\d+>}/edit", methods="GET|POST", name="admin_post_edit")
* @IsGranted("edit", subject="post", message="Posts can only be edited by their authors.")
*/
public function edit(Request $request, Post $post): Response
{
$form = $this->createForm(PostType::class, $post);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->getDoctrine()->getManager()->flush();
$this->addFlash('success', 'post.updated_successfully');
return $this->redirectToRoute('admin_post_edit', ['id' => $post->getId()]);
}
return $this->render('admin/blog/edit.html.twig', [
'post' => $post,
'form' => $form->createView(),
]);
}
/**
* Deletes a Post entity.
*
* @Route("/{id}/delete", methods="POST", name="admin_post_delete")
* @IsGranted("delete", subject="post")
*/
public function delete(Request $request, Post $post): Response
{
if (!$this->isCsrfTokenValid('delete', $request->request->get('token'))) {
return $this->redirectToRoute('admin_post_index');
}
// Delete the tags associated with this blog post. This is done automatically
// by Doctrine, except for SQLite (the database used in this application)
// because foreign key support is not enabled by default in SQLite
$post->getTags()->clear();
$em = $this->getDoctrine()->getManager();
$em->remove($post);
$em->flush();
$this->addFlash('success', 'post.deleted_successfully');
return $this->redirectToRoute('admin_post_index');
}
}

View File

@ -0,0 +1,170 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Controller;
use App\Entity\Comment;
use App\Entity\Post;
use App\Event\CommentCreatedEvent;
use App\Form\CommentType;
use App\Repository\PostRepository;
use App\Repository\TagRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* Controller used to manage blog contents in the public part of the site.
*
* @Route("/blog")
*
* @author Ryan Weaver <weaverryan@gmail.com>
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
class BlogController extends AbstractController
{
/**
* @Route("/", defaults={"page": "1", "_format"="html"}, methods="GET", name="blog_index")
* @Route("/rss.xml", defaults={"page": "1", "_format"="xml"}, methods="GET", name="blog_rss")
* @Route("/page/{page<[1-9]\d*>}", defaults={"_format"="html"}, methods="GET", name="blog_index_paginated")
* @Cache(smaxage="10")
*
* NOTE: For standard formats, Symfony will also automatically choose the best
* Content-Type header for the response.
* See https://symfony.com/doc/current/routing.html#special-parameters
*/
public function index(Request $request, int $page, string $_format, PostRepository $posts, TagRepository $tags): Response
{
$tag = null;
if ($request->query->has('tag')) {
$tag = $tags->findOneBy(['name' => $request->query->get('tag')]);
}
$latestPosts = $posts->findLatest($page, $tag);
// Every template name also has two extensions that specify the format and
// engine for that template.
// See https://symfony.com/doc/current/templates.html#template-naming
return $this->render('blog/index.'.$_format.'.twig', [
'paginator' => $latestPosts,
'tagName' => $tag ? $tag->getName() : null,
]);
}
/**
* @Route("/posts/{slug}", methods="GET", name="blog_post")
*
* NOTE: The $post controller argument is automatically injected by Symfony
* after performing a database query looking for a Post with the 'slug'
* value given in the route.
* See https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
*/
public function postShow(Post $post): Response
{
// Symfony's 'dump()' function is an improved version of PHP's 'var_dump()' but
// it's not available in the 'prod' environment to prevent leaking sensitive information.
// It can be used both in PHP files and Twig templates, but it requires to
// have enabled the DebugBundle. Uncomment the following line to see it in action:
//
// dump($post, $this->getUser(), new \DateTime());
return $this->render('blog/post_show.html.twig', ['post' => $post]);
}
/**
* @Route("/comment/{postSlug}/new", methods="POST", name="comment_new")
* @IsGranted("IS_AUTHENTICATED_FULLY")
* @ParamConverter("post", options={"mapping": {"postSlug": "slug"}})
*
* NOTE: The ParamConverter mapping is required because the route parameter
* (postSlug) doesn't match any of the Doctrine entity properties (slug).
* See https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html#doctrine-converter
*/
public function commentNew(Request $request, Post $post, EventDispatcherInterface $eventDispatcher): Response
{
$comment = new Comment();
$comment->setAuthor($this->getUser());
$post->addComment($comment);
$form = $this->createForm(CommentType::class, $comment);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($comment);
$em->flush();
// When an event is dispatched, Symfony notifies it to all the listeners
// and subscribers registered to it. Listeners can modify the information
// passed in the event and they can even modify the execution flow, so
// there's no guarantee that the rest of this controller will be executed.
// See https://symfony.com/doc/current/components/event_dispatcher.html
$eventDispatcher->dispatch(new CommentCreatedEvent($comment));
return $this->redirectToRoute('blog_post', ['slug' => $post->getSlug()]);
}
return $this->render('blog/comment_form_error.html.twig', [
'post' => $post,
'form' => $form->createView(),
]);
}
/**
* This controller is called directly via the render() function in the
* blog/post_show.html.twig template. That's why it's not needed to define
* a route name for it.
*
* The "id" of the Post is passed in and then turned into a Post object
* automatically by the ParamConverter.
*/
public function commentForm(Post $post): Response
{
$form = $this->createForm(CommentType::class);
return $this->render('blog/_comment_form.html.twig', [
'post' => $post,
'form' => $form->createView(),
]);
}
/**
* @Route("/search", methods="GET", name="blog_search")
*/
public function search(Request $request, PostRepository $posts): Response
{
$query = $request->query->get('q', '');
$limit = $request->query->get('l', 10);
if (!$request->isXmlHttpRequest()) {
return $this->render('blog/search.html.twig', ['query' => $query]);
}
$foundPosts = $posts->findBySearchQuery($query, $limit);
$results = [];
foreach ($foundPosts as $post) {
$results[] = [
'title' => htmlspecialchars($post->getTitle(), ENT_COMPAT | ENT_HTML5),
'date' => $post->getPublishedAt()->format('M d, Y'),
'author' => htmlspecialchars($post->getAuthor()->getFullName(), ENT_COMPAT | ENT_HTML5),
'summary' => htmlspecialchars($post->getSummary(), ENT_COMPAT | ENT_HTML5),
'url' => $this->generateUrl('blog_post', ['slug' => $post->getSlug()]),
];
}
return $this->json($results);
}
}

View File

@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
/**
* Controller used to manage the application security.
* See https://symfony.com/doc/current/security/form_login_setup.html.
*
* @author Ryan Weaver <weaverryan@gmail.com>
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
class SecurityController extends AbstractController
{
use TargetPathTrait;
/**
* @Route("/login", name="security_login")
*/
public function login(Request $request, Security $security, AuthenticationUtils $helper): Response
{
// if user is already logged in, don't display the login page again
if ($security->isGranted('ROLE_USER')) {
return $this->redirectToRoute('blog_index');
}
// this statement solves an edge-case: if you change the locale in the login
// page, after a successful login you are redirected to a page in the previous
// locale. This code regenerates the referrer URL whenever the login page is
// browsed, to ensure that its locale is always the current one.
$this->saveTargetPath($request->getSession(), 'main', $this->generateUrl('admin_index'));
return $this->render('security/login.html.twig', [
// last username entered by the user (if any)
'last_username' => $helper->getLastUsername(),
// last authentication error (if any)
'error' => $helper->getLastAuthenticationError(),
]);
}
/**
* This is the route the user can use to logout.
*
* But, this will never be executed. Symfony will intercept this first
* and handle the logout automatically. See logout in config/packages/security.yaml
*
* @Route("/logout", name="security_logout")
*/
public function logout(): void
{
throw new \Exception('This should never be reached!');
}
}

View File

@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Controller;
use App\Form\Type\ChangePasswordType;
use App\Form\UserType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
/**
* Controller used to manage current user.
*
* @Route("/profile")
* @IsGranted("ROLE_USER")
*
* @author Romain Monteil <monteil.romain@gmail.com>
*/
class UserController extends AbstractController
{
/**
* @Route("/edit", methods="GET|POST", name="user_edit")
*/
public function edit(Request $request): Response
{
$user = $this->getUser();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->getDoctrine()->getManager()->flush();
$this->addFlash('success', 'user.updated_successfully');
return $this->redirectToRoute('user_edit');
}
return $this->render('user/edit.html.twig', [
'user' => $user,
'form' => $form->createView(),
]);
}
/**
* @Route("/change-password", methods="GET|POST", name="user_change_password")
*/
public function changePassword(Request $request, UserPasswordEncoderInterface $encoder): Response
{
$user = $this->getUser();
$form = $this->createForm(ChangePasswordType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user->setPassword($encoder->encodePassword($user, $form->get('newPassword')->getData()));
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('security_logout');
}
return $this->render('user/change_password.html.twig', [
'form' => $form->createView(),
]);
}
}

View File

@ -0,0 +1,241 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\DataFixtures;
use App\Entity\Comment;
use App\Entity\Post;
use App\Entity\Tag;
use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\String\Slugger\SluggerInterface;
use function Symfony\Component\String\u;
class AppFixtures extends Fixture
{
private $passwordEncoder;
private $slugger;
public function __construct(UserPasswordEncoderInterface $passwordEncoder, SluggerInterface $slugger)
{
$this->passwordEncoder = $passwordEncoder;
$this->slugger = $slugger;
}
public function load(ObjectManager $manager): void
{
$this->loadUsers($manager);
$this->loadTags($manager);
$this->loadPosts($manager);
}
private function loadUsers(ObjectManager $manager): void
{
foreach ($this->getUserData() as [$fullname, $username, $password, $email, $roles]) {
$user = new User();
$user->setFullName($fullname);
$user->setUsername($username);
$user->setPassword($this->passwordEncoder->encodePassword($user, $password));
$user->setEmail($email);
$user->setRoles($roles);
$manager->persist($user);
$this->addReference($username, $user);
}
$manager->flush();
}
private function loadTags(ObjectManager $manager): void
{
foreach ($this->getTagData() as $index => $name) {
$tag = new Tag();
$tag->setName($name);
$manager->persist($tag);
$this->addReference('tag-'.$name, $tag);
}
$manager->flush();
}
private function loadPosts(ObjectManager $manager): void
{
foreach ($this->getPostData() as [$title, $slug, $summary, $content, $publishedAt, $author, $tags]) {
$post = new Post();
$post->setTitle($title);
$post->setSlug($slug);
$post->setSummary($summary);
$post->setContent($content);
$post->setPublishedAt($publishedAt);
$post->setAuthor($author);
$post->addTag(...$tags);
foreach (range(1, 5) as $i) {
$comment = new Comment();
$comment->setAuthor($this->getReference('john_user'));
$comment->setContent($this->getRandomText(random_int(255, 512)));
$comment->setPublishedAt(new \DateTime('now + '.$i.'seconds'));
$post->addComment($comment);
}
$manager->persist($post);
}
$manager->flush();
}
private function getUserData(): array
{
return [
// $userData = [$fullname, $username, $password, $email, $roles];
['Jane Doe', 'jane_admin', 'kitten', 'jane_admin@symfony.com', ['ROLE_ADMIN']],
['Tom Doe', 'tom_admin', 'kitten', 'tom_admin@symfony.com', ['ROLE_ADMIN']],
['John Doe', 'john_user', 'kitten', 'john_user@symfony.com', ['ROLE_USER']],
];
}
private function getTagData(): array
{
return [
'lorem',
'ipsum',
'consectetur',
'adipiscing',
'incididunt',
'labore',
'voluptate',
'dolore',
'pariatur',
];
}
private function getPostData()
{
$posts = [];
foreach ($this->getPhrases() as $i => $title) {
// $postData = [$title, $slug, $summary, $content, $publishedAt, $author, $tags, $comments];
$posts[] = [
$title,
$this->slugger->slug($title)->lower(),
$this->getRandomText(),
$this->getPostContent(),
new \DateTime('now - '.$i.'days'),
// Ensure that the first post is written by Jane Doe to simplify tests
$this->getReference(['jane_admin', 'tom_admin'][0 === $i ? 0 : random_int(0, 1)]),
$this->getRandomTags(),
];
}
return $posts;
}
private function getPhrases(): array
{
return [
'Lorem ipsum dolor sit amet consectetur adipiscing elit',
'Pellentesque vitae velit ex',
'Mauris dapibus risus quis suscipit vulputate',
'Eros diam egestas libero eu vulputate risus',
'In hac habitasse platea dictumst',
'Morbi tempus commodo mattis',
'Ut suscipit posuere justo at vulputate',
'Ut eleifend mauris et risus ultrices egestas',
'Aliquam sodales odio id eleifend tristique',
'Urna nisl sollicitudin id varius orci quam id turpis',
'Nulla porta lobortis ligula vel egestas',
'Curabitur aliquam euismod dolor non ornare',
'Sed varius a risus eget aliquam',
'Nunc viverra elit ac laoreet suscipit',
'Pellentesque et sapien pulvinar consectetur',
'Ubi est barbatus nix',
'Abnobas sunt hilotaes de placidus vita',
'Ubi est audax amicitia',
'Eposs sunt solems de superbus fortis',
'Vae humani generis',
'Diatrias tolerare tanquam noster caesium',
'Teres talis saepe tractare de camerarius flavum sensorem',
'Silva de secundus galatae demitto quadra',
'Sunt accentores vitare salvus flavum parses',
'Potus sensim ad ferox abnoba',
'Sunt seculaes transferre talis camerarius fluctuies',
'Era brevis ratione est',
'Sunt torquises imitari velox mirabilis medicinaes',
'Mineralis persuadere omnes finises desiderium',
'Bassus fatalis classiss virtualiter transferre de flavum',
];
}
private function getRandomText(int $maxLength = 255): string
{
$phrases = $this->getPhrases();
shuffle($phrases);
do {
$text = u('. ')->join($phrases)->append('.');
array_pop($phrases);
} while ($text->length() > $maxLength);
return $text;
}
private function getPostContent(): string
{
return <<<'MARKDOWN'
Lorem ipsum dolor sit amet consectetur adipisicing elit, sed do eiusmod tempor
incididunt ut labore et **dolore magna aliqua**: Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.
* Ut enim ad minim veniam
* Quis nostrud exercitation *ullamco laboris*
* Nisi ut aliquip ex ea commodo consequat
Praesent id fermentum lorem. Ut est lorem, fringilla at accumsan nec, euismod at
nunc. Aenean mattis sollicitudin mattis. Nullam pulvinar vestibulum bibendum.
Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos
himenaeos. Fusce nulla purus, gravida ac interdum ut, blandit eget ex. Duis a
luctus dolor.
Integer auctor massa maximus nulla scelerisque accumsan. *Aliquam ac malesuada*
ex. Pellentesque tortor magna, vulputate eu vulputate ut, venenatis ac lectus.
Praesent ut lacinia sem. Mauris a lectus eget felis mollis feugiat. Quisque
efficitur, mi ut semper pulvinar, urna urna blandit massa, eget tincidunt augue
nulla vitae est.
Ut posuere aliquet tincidunt. Aliquam erat volutpat. **Class aptent taciti**
sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Morbi
arcu orci, gravida eget aliquam eu, suscipit et ante. Morbi vulputate metus vel
ipsum finibus, ut dapibus massa feugiat. Vestibulum vel lobortis libero. Sed
tincidunt tellus et viverra scelerisque. Pellentesque tincidunt cursus felis.
Sed in egestas erat.
Aliquam pulvinar interdum massa, vel ullamcorper ante consectetur eu. Vestibulum
lacinia ac enim vel placerat. Integer pulvinar magna nec dui malesuada, nec
congue nisl dictum. Donec mollis nisl tortor, at congue erat consequat a. Nam
tempus elit porta, blandit elit vel, viverra lorem. Sed sit amet tellus
tincidunt, faucibus nisl in, aliquet libero.
MARKDOWN;
}
private function getRandomTags(): array
{
$tagNames = $this->getTagData();
shuffle($tagNames);
$selectedTags = \array_slice($tagNames, 0, random_int(2, 4));
return array_map(function ($tagName) { return $this->getReference('tag-'.$tagName); }, $selectedTags);
}
}

138
app/src/Entity/Comment.php Normal file
View File

@ -0,0 +1,138 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use function Symfony\Component\String\u;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity
* @ORM\Table(name="symfony_demo_comment")
*
* Defines the properties of the Comment entity to represent the blog comments.
* See https://symfony.com/doc/current/doctrine.html#creating-an-entity-class
*
* Tip: if you have an existing database, you can generate these entity class automatically.
* See https://symfony.com/doc/current/doctrine/reverse_engineering.html
*
* @author Ryan Weaver <weaverryan@gmail.com>
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
class Comment
{
/**
* @var int
*
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @var Post
*
* @ORM\ManyToOne(targetEntity="Post", inversedBy="comments")
* @ORM\JoinColumn(nullable=false)
*/
private $post;
/**
* @var string
*
* @ORM\Column(type="text")
* @Assert\NotBlank(message="comment.blank")
* @Assert\Length(
* min=5,
* minMessage="comment.too_short",
* max=10000,
* maxMessage="comment.too_long"
* )
*/
private $content;
/**
* @var \DateTime
*
* @ORM\Column(type="datetime")
*/
private $publishedAt;
/**
* @var User
*
* @ORM\ManyToOne(targetEntity="App\Entity\User")
* @ORM\JoinColumn(nullable=false)
*/
private $author;
public function __construct()
{
$this->publishedAt = new \DateTime();
}
/**
* @Assert\IsTrue(message="comment.is_spam")
*/
public function isLegitComment(): bool
{
$containsInvalidCharacters = null !== u($this->content)->indexOf('@');
return !$containsInvalidCharacters;
}
public function getId(): ?int
{
return $this->id;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): void
{
$this->content = $content;
}
public function getPublishedAt(): \DateTime
{
return $this->publishedAt;
}
public function setPublishedAt(\DateTime $publishedAt): void
{
$this->publishedAt = $publishedAt;
}
public function getAuthor(): ?User
{
return $this->author;
}
public function setAuthor(User $author): void
{
$this->author = $author;
}
public function getPost(): ?Post
{
return $this->post;
}
public function setPost(Post $post): void
{
$this->post = $post;
}
}

226
app/src/Entity/Post.php Normal file
View File

@ -0,0 +1,226 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\PostRepository")
* @ORM\Table(name="symfony_demo_post")
* @UniqueEntity(fields={"slug"}, errorPath="title", message="post.slug_unique")
*
* Defines the properties of the Post entity to represent the blog posts.
*
* See https://symfony.com/doc/current/doctrine.html#creating-an-entity-class
*
* Tip: if you have an existing database, you can generate these entity class automatically.
* See https://symfony.com/doc/current/doctrine/reverse_engineering.html
*
* @author Ryan Weaver <weaverryan@gmail.com>
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class Post
{
/**
* @var int
*
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @var string
*
* @ORM\Column(type="string")
* @Assert\NotBlank
*/
private $title;
/**
* @var string
*
* @ORM\Column(type="string")
*/
private $slug;
/**
* @var string
*
* @ORM\Column(type="string")
* @Assert\NotBlank(message="post.blank_summary")
* @Assert\Length(max=255)
*/
private $summary;
/**
* @var string
*
* @ORM\Column(type="text")
* @Assert\NotBlank(message="post.blank_content")
* @Assert\Length(min=10, minMessage="post.too_short_content")
*/
private $content;
/**
* @var \DateTime
*
* @ORM\Column(type="datetime")
*/
private $publishedAt;
/**
* @var User
*
* @ORM\ManyToOne(targetEntity="App\Entity\User")
* @ORM\JoinColumn(nullable=false)
*/
private $author;
/**
* @var Comment[]|Collection
*
* @ORM\OneToMany(
* targetEntity="Comment",
* mappedBy="post",
* orphanRemoval=true,
* cascade={"persist"}
* )
* @ORM\OrderBy({"publishedAt": "DESC"})
*/
private $comments;
/**
* @var Tag[]|Collection
*
* @ORM\ManyToMany(targetEntity="App\Entity\Tag", cascade={"persist"})
* @ORM\JoinTable(name="symfony_demo_post_tag")
* @ORM\OrderBy({"name": "ASC"})
* @Assert\Count(max="4", maxMessage="post.too_many_tags")
*/
private $tags;
public function __construct()
{
$this->publishedAt = new \DateTime();
$this->comments = new ArrayCollection();
$this->tags = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(?string $title): void
{
$this->title = $title;
}
public function getSlug(): ?string
{
return $this->slug;
}
public function setSlug(string $slug): void
{
$this->slug = $slug;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(?string $content): void
{
$this->content = $content;
}
public function getPublishedAt(): \DateTime
{
return $this->publishedAt;
}
public function setPublishedAt(\DateTime $publishedAt): void
{
$this->publishedAt = $publishedAt;
}
public function getAuthor(): ?User
{
return $this->author;
}
public function setAuthor(User $author): void
{
$this->author = $author;
}
public function getComments(): Collection
{
return $this->comments;
}
public function addComment(Comment $comment): void
{
$comment->setPost($this);
if (!$this->comments->contains($comment)) {
$this->comments->add($comment);
}
}
public function removeComment(Comment $comment): void
{
$this->comments->removeElement($comment);
}
public function getSummary(): ?string
{
return $this->summary;
}
public function setSummary(?string $summary): void
{
$this->summary = $summary;
}
public function addTag(Tag ...$tags): void
{
foreach ($tags as $tag) {
if (!$this->tags->contains($tag)) {
$this->tags->add($tag);
}
}
}
public function removeTag(Tag $tag): void
{
$this->tags->removeElement($tag);
}
public function getTags(): Collection
{
return $this->tags;
}
}

75
app/src/Entity/Tag.php Normal file
View File

@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity()
* @ORM\Table(name="symfony_demo_tag")
*
* Defines the properties of the Tag entity to represent the post tags.
*
* See https://symfony.com/doc/current/doctrine.html#creating-an-entity-class
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class Tag implements \JsonSerializable
{
/**
* @var int
*
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @var string
*
* @ORM\Column(type="string", unique=true)
*/
private $name;
public function getId(): ?int
{
return $this->id;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getName(): ?string
{
return $this->name;
}
/**
* {@inheritdoc}
*/
public function jsonSerialize(): string
{
// This entity implements JsonSerializable (http://php.net/manual/en/class.jsonserializable.php)
// so this method is used to customize its JSON representation when json_encode()
// is called, for example in tags|json_encode (templates/form/fields.html.twig)
return $this->name;
}
public function __toString(): string
{
return $this->name;
}
}

188
app/src/Entity/User.php Normal file
View File

@ -0,0 +1,188 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
* @ORM\Table(name="symfony_demo_user")
*
* Defines the properties of the User entity to represent the application users.
* See https://symfony.com/doc/current/doctrine.html#creating-an-entity-class
*
* Tip: if you have an existing database, you can generate these entity class automatically.
* See https://symfony.com/doc/current/doctrine/reverse_engineering.html
*
* @author Ryan Weaver <weaverryan@gmail.com>
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
class User implements UserInterface, \Serializable
{
/**
* @var int
*
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @var string
*
* @ORM\Column(type="string")
* @Assert\NotBlank()
*/
private $fullName;
/**
* @var string
*
* @ORM\Column(type="string", unique=true)
* @Assert\NotBlank()
* @Assert\Length(min=2, max=50)
*/
private $username;
/**
* @var string
*
* @ORM\Column(type="string", unique=true)
* @Assert\Email()
*/
private $email;
/**
* @var string
*
* @ORM\Column(type="string")
*/
private $password;
/**
* @var array
*
* @ORM\Column(type="json")
*/
private $roles = [];
public function getId(): ?int
{
return $this->id;
}
public function setFullName(string $fullName): void
{
$this->fullName = $fullName;
}
public function getFullName(): ?string
{
return $this->fullName;
}
public function getUsername(): ?string
{
return $this->username;
}
public function setUsername(string $username): void
{
$this->username = $username;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): void
{
$this->email = $email;
}
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(string $password): void
{
$this->password = $password;
}
/**
* Returns the roles or permissions granted to the user for security.
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantees that a user always has at least one role for security
if (empty($roles)) {
$roles[] = 'ROLE_USER';
}
return array_unique($roles);
}
public function setRoles(array $roles): void
{
$this->roles = $roles;
}
/**
* Returns the salt that was originally used to encode the password.
*
* {@inheritdoc}
*/
public function getSalt(): ?string
{
// We're using bcrypt in security.yaml to encode the password, so
// the salt value is built-in and and you don't have to generate one
// See https://en.wikipedia.org/wiki/Bcrypt
return null;
}
/**
* Removes sensitive data from the user.
*
* {@inheritdoc}
*/
public function eraseCredentials(): void
{
// if you had a plainPassword property, you'd nullify it here
// $this->plainPassword = null;
}
/**
* {@inheritdoc}
*/
public function serialize(): string
{
// add $this->salt too if you don't use Bcrypt or Argon2i
return serialize([$this->id, $this->username, $this->password]);
}
/**
* {@inheritdoc}
*/
public function unserialize($serialized): void
{
// add $this->salt too if you don't use Bcrypt or Argon2i
[$this->id, $this->username, $this->password] = unserialize($serialized, ['allowed_classes' => false]);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Event;
use App\Entity\Comment;
use Symfony\Contracts\EventDispatcher\Event;
class CommentCreatedEvent extends Event
{
protected $comment;
public function __construct(Comment $comment)
{
$this->comment = $comment;
}
public function getComment(): Comment
{
return $this->comment;
}
}

View File

@ -0,0 +1,101 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* This application uses by default an SQLite database to store its information.
* That's why the 'sqlite3' extension must be enabled in PHP. This event
* subscriber listens to console events and in case of an exception caused by
* a disabled 'sqlite3' extension, it displays a meaningful error message.
*
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
class CheckRequirementsSubscriber implements EventSubscriberInterface
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
// Event Subscribers must define this method to declare the events they
// listen to. You can listen to several events, execute more than one method
// for each event and set the priority of each event too.
// See https://symfony.com/doc/current/event_dispatcher.html#creating-an-event-subscriber
public static function getSubscribedEvents(): array
{
return [
// Errors are one of the events defined by the Console. See the
// rest here: https://symfony.com/doc/current/components/console/events.html
ConsoleEvents::ERROR => 'handleConsoleError',
// See: https://symfony.com/doc/current/components/http_kernel.html#component-http-kernel-event-table
KernelEvents::EXCEPTION => 'handleKernelException',
];
}
/**
* This method checks if there has been an error in a command related to
* the database and then, it checks if the 'sqlite3' PHP extension is enabled
* or not to display a better error message.
*/
public function handleConsoleError(ConsoleErrorEvent $event): void
{
$commandNames = ['doctrine:fixtures:load', 'doctrine:database:create', 'doctrine:schema:create', 'doctrine:database:drop'];
if ($event->getCommand() && \in_array($event->getCommand()->getName(), $commandNames, true)) {
if ($this->isSQLitePlatform() && !\extension_loaded('sqlite3')) {
$io = new SymfonyStyle($event->getInput(), $event->getOutput());
$io->error('This command requires to have the "sqlite3" PHP extension enabled because, by default, the Symfony Demo application uses SQLite to store its information.');
}
}
}
/**
* This method checks if the triggered exception is related to the database
* and then, it checks if the required 'sqlite3' PHP extension is enabled.
*/
public function handleKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
// Since any exception thrown during a Twig template rendering is wrapped
// in a Twig_Error_Runtime, we must get the original exception.
$previousException = $exception->getPrevious();
// Driver exception may happen in controller or in twig template rendering
$isDriverException = ($exception instanceof DriverException || $previousException instanceof DriverException);
// Check if SQLite is enabled
if ($isDriverException && $this->isSQLitePlatform() && !\extension_loaded('sqlite3')) {
$event->setThrowable(new \Exception('PHP extension "sqlite3" must be enabled because, by default, the Symfony Demo application uses SQLite to store its information.'));
}
}
/**
* Checks if the application is using SQLite as its database.
*/
private function isSQLitePlatform(): bool
{
$databasePlatform = $this->entityManager->getConnection()->getDatabasePlatform();
return $databasePlatform ? 'sqlite' === $databasePlatform->getName() : false;
}
}

View File

@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber;
use App\Entity\Comment;
use App\Event\CommentCreatedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Notifies post's author about new comments.
*
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
*/
class CommentNotificationSubscriber implements EventSubscriberInterface
{
private $mailer;
private $translator;
private $urlGenerator;
private $sender;
public function __construct(MailerInterface $mailer, UrlGeneratorInterface $urlGenerator, TranslatorInterface $translator, string $sender)
{
$this->mailer = $mailer;
$this->urlGenerator = $urlGenerator;
$this->translator = $translator;
$this->sender = $sender;
}
public static function getSubscribedEvents(): array
{
return [
CommentCreatedEvent::class => 'onCommentCreated',
];
}
public function onCommentCreated(CommentCreatedEvent $event): void
{
/** @var Comment $comment */
$comment = $event->getComment();
$post = $comment->getPost();
$linkToPost = $this->urlGenerator->generate('blog_post', [
'slug' => $post->getSlug(),
'_fragment' => 'comment_'.$comment->getId(),
], UrlGeneratorInterface::ABSOLUTE_URL);
$subject = $this->translator->trans('notification.comment_created');
$body = $this->translator->trans('notification.comment_created.description', [
'%title%' => $post->getTitle(),
'%link%' => $linkToPost,
]);
// See https://symfony.com/doc/current/mailer.html
$email = (new Email())
->from($this->sender)
->to($post->getAuthor()->getEmail())
->subject($subject)
->html($body)
;
// In config/packages/dev/mailer.yaml the delivery of messages is disabled.
// That's why in the development environment you won't actually receive any email.
// However, you can inspect the contents of those unsent emails using the debug toolbar.
$this->mailer->send($email);
}
}

View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber;
use App\Twig\SourceCodeExtension;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Defines the method that 'listens' to the 'kernel.controller' event, which is
* triggered whenever a controller is executed in the application.
*
* @author Ryan Weaver <weaverryan@gmail.com>
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
class ControllerSubscriber implements EventSubscriberInterface
{
private $twigExtension;
public function __construct(SourceCodeExtension $twigExtension)
{
$this->twigExtension = $twigExtension;
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::CONTROLLER => 'registerCurrentController',
];
}
public function registerCurrentController(ControllerEvent $event): void
{
// this check is needed because in Symfony a request can perform any
// number of sub-requests. See
// https://symfony.com/doc/current/components/http_kernel.html#sub-requests
if ($event->isMasterRequest()) {
$this->twigExtension->setController($event->getController());
}
}
}

View File

@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use function Symfony\Component\String\u;
/**
* When visiting the homepage, this listener redirects the user to the most
* appropriate localized version according to the browser settings.
*
* See https://symfony.com/doc/current/components/http_kernel.html#the-kernel-request-event
*
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
*/
class RedirectToPreferredLocaleSubscriber implements EventSubscriberInterface
{
private $urlGenerator;
private $locales;
private $defaultLocale;
public function __construct(UrlGeneratorInterface $urlGenerator, string $locales, string $defaultLocale = null)
{
$this->urlGenerator = $urlGenerator;
$this->locales = explode('|', trim($locales));
if (empty($this->locales)) {
throw new \UnexpectedValueException('The list of supported locales must not be empty.');
}
$this->defaultLocale = $defaultLocale ?: $this->locales[0];
if (!\in_array($this->defaultLocale, $this->locales, true)) {
throw new \UnexpectedValueException(sprintf('The default locale ("%s") must be one of "%s".', $this->defaultLocale, $locales));
}
// Add the default locale at the first position of the array,
// because Symfony\HttpFoundation\Request::getPreferredLanguage
// returns the first element when no an appropriate language is found
array_unshift($this->locales, $this->defaultLocale);
$this->locales = array_unique($this->locales);
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => 'onKernelRequest',
];
}
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
// Ignore sub-requests and all URLs but the homepage
if (!$event->isMasterRequest() || '/' !== $request->getPathInfo()) {
return;
}
// Ignore requests from referrers with the same HTTP host in order to prevent
// changing language for users who possibly already selected it for this application.
$referrer = $request->headers->get('referer');
if (null !== $referrer && u($referrer)->ignoreCase()->startsWith($request->getSchemeAndHttpHost())) {
return;
}
$preferredLanguage = $request->getPreferredLanguage($this->locales);
if ($preferredLanguage !== $this->defaultLocale) {
$response = new RedirectResponse($this->urlGenerator->generate('homepage', ['_locale' => $preferredLanguage]));
$event->setResponse($response);
}
}
}

View File

@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form;
use App\Entity\Comment;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used to create and manipulate blog comments. Although in this
* case the form is trivial and we could build it inside the controller, a good
* practice is to always define your forms as classes.
*
* See https://symfony.com/doc/current/forms.html#creating-form-classes
*
* @author Ryan Weaver <weaverryan@gmail.com>
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
class CommentType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// By default, form fields include the 'required' attribute, which enables
// the client-side form validation. This means that you can't test the
// server-side validation errors from the browser. To temporarily disable
// this validation, set the 'required' attribute to 'false':
// $builder->add('content', null, ['required' => false]);
$builder
->add('content', TextareaType::class, [
'help' => 'help.comment_content',
])
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Comment::class,
]);
}
}

View File

@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form\DataTransformer;
use App\Entity\Tag;
use App\Repository\TagRepository;
use Symfony\Component\Form\DataTransformerInterface;
use function Symfony\Component\String\u;
/**
* This data transformer is used to translate the array of tags into a comma separated format
* that can be displayed and managed by Bootstrap-tagsinput js plugin (and back on submit).
*
* See https://symfony.com/doc/current/form/data_transformers.html
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
* @author Jonathan Boyer <contact@grafikart.fr>
*/
class TagArrayToStringTransformer implements DataTransformerInterface
{
private $tags;
public function __construct(TagRepository $tags)
{
$this->tags = $tags;
}
/**
* {@inheritdoc}
*/
public function transform($tags): string
{
// The value received is an array of Tag objects generated with
// Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::transform()
// The value returned is a string that concatenates the string representation of those objects
/* @var Tag[] $tags */
return implode(',', $tags);
}
/**
* {@inheritdoc}
*/
public function reverseTransform($string): array
{
if (null === $string || u($string)->isEmpty()) {
return [];
}
$names = array_filter(array_unique(array_map('trim', u($string)->split(','))));
// Get the current tags and find the new ones that should be created.
$tags = $this->tags->findBy([
'name' => $names,
]);
$newNames = array_diff($names, $tags);
foreach ($newNames as $name) {
$tag = new Tag();
$tag->setName($name);
$tags[] = $tag;
// There's no need to persist these new tags because Doctrine does that automatically
// thanks to the cascade={"persist"} option in the App\Entity\Post::$tags property.
}
// Return an array of tags to transform them back into a Doctrine Collection.
// See Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::reverseTransform()
return $tags;
}
}

100
app/src/Form/PostType.php Normal file
View File

@ -0,0 +1,100 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form;
use App\Entity\Post;
use App\Form\Type\DateTimePickerType;
use App\Form\Type\TagsInputType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\String\Slugger\SluggerInterface;
/**
* Defines the form used to create and manipulate blog posts.
*
* @author Ryan Weaver <weaverryan@gmail.com>
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class PostType extends AbstractType
{
private $slugger;
// Form types are services, so you can inject other services in them if needed
public function __construct(SluggerInterface $slugger)
{
$this->slugger = $slugger;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// For the full reference of options defined by each form field type
// see https://symfony.com/doc/current/reference/forms/types.html
// By default, form fields include the 'required' attribute, which enables
// the client-side form validation. This means that you can't test the
// server-side validation errors from the browser. To temporarily disable
// this validation, set the 'required' attribute to 'false':
// $builder->add('title', null, ['required' => false, ...]);
$builder
->add('title', null, [
'attr' => ['autofocus' => true],
'label' => 'label.title',
])
->add('summary', TextareaType::class, [
'help' => 'help.post_summary',
'label' => 'label.summary',
])
->add('content', null, [
'attr' => ['rows' => 20],
'help' => 'help.post_content',
'label' => 'label.content',
])
->add('publishedAt', DateTimePickerType::class, [
'label' => 'label.published_at',
'help' => 'help.post_publication',
])
->add('tags', TagsInputType::class, [
'label' => 'label.tags',
'required' => false,
])
// form events let you modify information or fields at different steps
// of the form handling process.
// See https://symfony.com/doc/current/form/events.html
->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) {
/** @var Post */
$post = $event->getData();
if (null !== $postTitle = $post->getTitle()) {
$post->setSlug($this->slugger->slug($postTitle)->lower());
}
})
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Post::class,
]);
}
}

View File

@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* Defines the custom form field type used to change user's password.
*
* @author Romain Monteil <monteil.romain@gmail.com>
*/
class ChangePasswordType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('currentPassword', PasswordType::class, [
'constraints' => [
new UserPassword(),
],
'label' => 'label.current_password',
'attr' => [
'autocomplete' => 'off',
],
])
->add('newPassword', RepeatedType::class, [
'type' => PasswordType::class,
'constraints' => [
new NotBlank(),
new Length([
'min' => 5,
'max' => 128,
]),
],
'first_options' => [
'label' => 'label.new_password',
],
'second_options' => [
'label' => 'label.new_password_confirm',
],
])
;
}
}

View File

@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form\Type;
use App\Utils\MomentFormatConverter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use function Symfony\Component\String\u;
/**
* Defines the custom form field type used to manipulate datetime values across
* Bootstrap Date\Time Picker javascript plugin.
*
* See https://symfony.com/doc/current/form/create_custom_field_type.html
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class DateTimePickerType extends AbstractType
{
private $formatConverter;
public function __construct(MomentFormatConverter $converter)
{
$this->formatConverter = $converter;
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['attr']['data-date-format'] = $this->formatConverter->convert($options['format']);
$view->vars['attr']['data-date-locale'] = u(\Locale::getDefault())->replace('_', '-')->lower();
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'widget' => 'single_text',
// if true, the browser will display the native date picker widget
// however, this app uses a custom JavaScript widget, so it must be set to false
'html5' => false,
]);
}
/**
* {@inheritdoc}
*/
public function getParent(): ?string
{
return DateTimeType::class;
}
}

View File

@ -0,0 +1,70 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form\Type;
use App\Form\DataTransformer\TagArrayToStringTransformer;
use App\Repository\TagRepository;
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
/**
* Defines the custom form field type used to manipulate tags values across
* Bootstrap-tagsinput javascript plugin.
*
* See https://symfony.com/doc/current/form/create_custom_field_type.html
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class TagsInputType extends AbstractType
{
private $tags;
public function __construct(TagRepository $tags)
{
$this->tags = $tags;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
// The Tag collection must be transformed into a comma separated string.
// We could create a custom transformer to do Collection <-> string in one step,
// but here we're doing the transformation in two steps (Collection <-> array <-> string)
// and reuse the existing CollectionToArrayTransformer.
->addModelTransformer(new CollectionToArrayTransformer(), true)
->addModelTransformer(new TagArrayToStringTransformer($this->tags), true)
;
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['tags'] = $this->tags->findAll();
}
/**
* {@inheritdoc}
*/
public function getParent(): ?string
{
return TextType::class;
}
}

65
app/src/Form/UserType.php Normal file
View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used to edit an user.
*
* @author Romain Monteil <monteil.romain@gmail.com>
*/
class UserType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// For the full reference of options defined by each form field type
// see https://symfony.com/doc/current/reference/forms/types.html
// By default, form fields include the 'required' attribute, which enables
// the client-side form validation. This means that you can't test the
// server-side validation errors from the browser. To temporarily disable
// this validation, set the 'required' attribute to 'false':
// $builder->add('title', null, ['required' => false, ...]);
$builder
->add('username', TextType::class, [
'label' => 'label.username',
'disabled' => true,
])
->add('fullName', TextType::class, [
'label' => 'label.fullname',
])
->add('email', EmailType::class, [
'label' => 'label.email',
])
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}

47
app/src/Kernel.php Normal file
View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
protected function configureContainer(ContainerConfigurator $container): void
{
$container->import('../config/{packages}/*.yaml');
$container->import('../config/{packages}/'.$this->environment.'/*.yaml');
if (is_file(\dirname(__DIR__).'/config/services.yaml')) {
$container->import('../config/services.yaml');
$container->import('../config/{services}_'.$this->environment.'.yaml');
} elseif (is_file($path = \dirname(__DIR__).'/config/services.php')) {
(require $path)($container->withPath($path), $this);
}
}
protected function configureRoutes(RoutingConfigurator $routes): void
{
$routes->import('../config/{routes}/'.$this->environment.'/*.yaml');
$routes->import('../config/{routes}/*.yaml');
if (is_file(\dirname(__DIR__).'/config/routes.yaml')) {
$routes->import('../config/routes.yaml');
} elseif (is_file($path = \dirname(__DIR__).'/config/routes.php')) {
(require $path)($routes->withPath($path), $this);
}
}
}

View File

@ -0,0 +1,117 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Pagination;
use Doctrine\ORM\QueryBuilder as DoctrineQueryBuilder;
use Doctrine\ORM\Tools\Pagination\CountWalker;
use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator;
/**
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
class Paginator
{
/**
* Use constants to define configuration options that rarely change instead
* of specifying them under parameters section in config/services.yaml file.
*
* See https://symfony.com/doc/current/best_practices.html#use-constants-to-define-options-that-rarely-change
*/
public const PAGE_SIZE = 10;
private $queryBuilder;
private $currentPage;
private $pageSize;
private $results;
private $numResults;
public function __construct(DoctrineQueryBuilder $queryBuilder, int $pageSize = self::PAGE_SIZE)
{
$this->queryBuilder = $queryBuilder;
$this->pageSize = $pageSize;
}
public function paginate(int $page = 1): self
{
$this->currentPage = max(1, $page);
$firstResult = ($this->currentPage - 1) * $this->pageSize;
$query = $this->queryBuilder
->setFirstResult($firstResult)
->setMaxResults($this->pageSize)
->getQuery();
if (0 === \count($this->queryBuilder->getDQLPart('join'))) {
$query->setHint(CountWalker::HINT_DISTINCT, false);
}
$paginator = new DoctrinePaginator($query, true);
$useOutputWalkers = \count($this->queryBuilder->getDQLPart('having') ?: []) > 0;
$paginator->setUseOutputWalkers($useOutputWalkers);
$this->results = $paginator->getIterator();
$this->numResults = $paginator->count();
return $this;
}
public function getCurrentPage(): int
{
return $this->currentPage;
}
public function getLastPage(): int
{
return (int) ceil($this->numResults / $this->pageSize);
}
public function getPageSize(): int
{
return $this->pageSize;
}
public function hasPreviousPage(): bool
{
return $this->currentPage > 1;
}
public function getPreviousPage(): int
{
return max(1, $this->currentPage - 1);
}
public function hasNextPage(): bool
{
return $this->currentPage < $this->getLastPage();
}
public function getNextPage(): int
{
return min($this->getLastPage(), $this->currentPage + 1);
}
public function hasToPaginate(): bool
{
return $this->numResults > $this->pageSize;
}
public function getNumResults(): int
{
return $this->numResults;
}
public function getResults(): \Traversable
{
return $this->results;
}
}

View File

@ -0,0 +1,97 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Repository;
use App\Entity\Post;
use App\Entity\Tag;
use App\Pagination\Paginator;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use function Symfony\Component\String\u;
/**
* This custom Doctrine repository contains some methods which are useful when
* querying for blog post information.
*
* See https://symfony.com/doc/current/doctrine.html#querying-for-objects-the-repository
*
* @author Ryan Weaver <weaverryan@gmail.com>
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class PostRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Post::class);
}
public function findLatest(int $page = 1, Tag $tag = null): Paginator
{
$qb = $this->createQueryBuilder('p')
->addSelect('a', 't')
->innerJoin('p.author', 'a')
->leftJoin('p.tags', 't')
->where('p.publishedAt <= :now')
->orderBy('p.publishedAt', 'DESC')
->setParameter('now', new \DateTime())
;
if (null !== $tag) {
$qb->andWhere(':tag MEMBER OF p.tags')
->setParameter('tag', $tag);
}
return (new Paginator($qb))->paginate($page);
}
/**
* @return Post[]
*/
public function findBySearchQuery(string $query, int $limit = Paginator::PAGE_SIZE): array
{
$searchTerms = $this->extractSearchTerms($query);
if (0 === \count($searchTerms)) {
return [];
}
$queryBuilder = $this->createQueryBuilder('p');
foreach ($searchTerms as $key => $term) {
$queryBuilder
->orWhere('p.title LIKE :t_'.$key)
->setParameter('t_'.$key, '%'.$term.'%')
;
}
return $queryBuilder
->orderBy('p.publishedAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
/**
* Transforms the search string into an array of search terms.
*/
private function extractSearchTerms(string $searchQuery): array
{
$searchQuery = u($searchQuery)->replaceMatches('/[[:space:]]+/', ' ')->trim();
$terms = array_unique($searchQuery->split(' '));
// ignore the search terms that are too short
return array_filter($terms, function ($term) {
return 2 <= $term->length();
});
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Repository;
use App\Entity\Tag;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* This custom Doctrine repository is empty because so far we don't need any custom
* method to query for application user information. But it's always a good practice
* to define a custom repository that will be used when the application grows.
*
* See https://symfony.com/doc/current/doctrine.html#querying-for-objects-the-repository
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class TagRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Tag::class);
}
}

View File

@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* This custom Doctrine repository is empty because so far we don't need any custom
* method to query for application user information. But it's always a good practice
* to define a custom repository that will be used when the application grows.
*
* See https://symfony.com/doc/current/doctrine.html#querying-for-objects-the-repository
*
* @author Ryan Weaver <weaverryan@gmail.com>
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
class UserRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
}

Some files were not shown because too many files have changed in this diff Show More